From b5db358d2dc13119d0158a424950fb4a8aa2e552 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Fri, 15 Nov 2024 17:28:55 -0500 Subject: [PATCH 01/24] convert create/upgrade app calls to sqlfacade; TODO: refactor test_run_processor --- .../nativeapp/entities/application.py | 48 +-- .../cli/_plugins/nativeapp/sf_sql_facade.py | 81 +++++ src/snowflake/cli/api/entities/utils.py | 9 +- src/snowflake/cli/api/errno.py | 1 + tests/nativeapp/test_sf_sql_facade.py | 288 +++++++++++++++++- 5 files changed, 385 insertions(+), 42 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index 040e60e82f..c4f653afe5 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -30,7 +30,6 @@ COMMENT_COL, NAME_COL, OWNER_COL, - SPECIAL_COMMENT, ) from snowflake.cli._plugins.nativeapp.entities.application_package import ( ApplicationPackageEntity, @@ -678,11 +677,12 @@ def create_or_upgrade_app( 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}", + upgrade_result = get_snowflake_facade().upgrade_application( + name=self.name, + install_method=install_method, + stage_fqn=stage_fqn, ) - print_messages(console, upgrade_cursor) + print_messages(console, upgrade_result) events_definitions = ( get_snowflake_facade().get_event_definitions( @@ -755,37 +755,15 @@ def create_or_upgrade_app( ) 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}" - - 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} - """ - ), + create_result = get_snowflake_facade().create_application( + name=self.name, + package_name=package.name, + install_method=install_method, + stage_fqn=stage_fqn, + debug_mode=debug_mode, + new_authorize_event_sharing_value=event_sharing.should_authorize_event_sharing_during_create(), ) - print_messages(console, create_cursor) + print_messages(console, create_result) events_definitions = get_snowflake_facade().get_event_definitions( self.name, self.role ) diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index b8cd77dbee..509acab723 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -18,15 +18,21 @@ from textwrap import dedent from typing import Any, Dict, List +from snowflake.cli._plugins.nativeapp.constants import 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, InsufficientPrivilegesError, UnexpectedResultError, + UserInputError, UserScriptError, handle_unclassified_error, ) from snowflake.cli.api.errno import ( + APPLICATION_REQUIRES_TELEMETRY_SHARING, DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, @@ -503,6 +509,81 @@ def show_release_directives( ) return cursor.fetchall() + def upgrade_application( + self, name: str, install_method: SameAccountInstallMethod, stage_fqn: str + ): + """ + Upgrades an application object using the provided clause + """ + try: + using_clause = install_method.using_clause(stage_fqn) + upgrade_cursor = self._sql_executor.execute_query( + f"alter application {name} upgrade {using_clause}", + ) + return upgrade_cursor.fetchall() + except ProgrammingError as err: + if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING: + # this needs to be bubbled up so that it can be handled by the EventSharingHandler + raise + raise UserInputError( + f"Failed to upgrade application {name} with the following error message:\n" + f"{err.msg}" + ) from err + except Exception as err: + handle_unclassified_error(err, f"Failed to upgrade application {name}.") + + def create_application( + self, + name: str, + package_name: str, + install_method: SameAccountInstallMethod, + stage_fqn: str, + debug_mode: bool | None, + new_authorize_event_sharing_value: bool | None, + ): + """ + Creates a new application object using an application package, + running the setup script of the application package + """ + # 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 new_authorize_event_sharing_value is not None: + self._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) + + 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} + """ + ), + ) + return create_cursor.fetchall() + except ProgrammingError as err: + if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING: + # this needs to be bubbled up so that it can be handled by the EventSharingHandler + raise + raise UserInputError( + f"Failed to create application {name} with the following error message:\n" + f"{err.msg}" + ) from err + except Exception as err: + handle_unclassified_error(err, f"Failed to create application {name}.") + # 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/entities/utils.py b/src/snowflake/cli/api/entities/utils.py index 4c5b8b0c78..9507d5b689 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): """ 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..36ebaad151 100644 --- a/src/snowflake/cli/api/errno.py +++ b/src/snowflake/cli/api/errno.py @@ -28,3 +28,4 @@ APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128 APPLICATION_REQUIRES_TELEMETRY_SHARING = 93321 CANNOT_DISABLE_MANDATORY_TELEMETRY = 93329 +APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT = 93082 diff --git a/tests/nativeapp/test_sf_sql_facade.py b/tests/nativeapp/test_sf_sql_facade.py index ec5c23cb50..006a416319 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -17,6 +17,10 @@ from unittest.mock import _Call as Call import pytest +from snowflake.cli._plugins.nativeapp.constants import 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,12 +28,15 @@ InvalidSQLError, UnknownConnectorError, UnknownSQLError, + UserInputError, UserScriptError, ) from snowflake.cli._plugins.nativeapp.sf_sql_facade import ( SnowflakeSQLFacade, ) from snowflake.cli.api.errno import ( + APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + APPLICATION_REQUIRES_TELEMETRY_SHARING, DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, @@ -47,7 +54,7 @@ mock_execute_helper, ) -sql_facade = None +sql_facade = SnowflakeSQLFacade() @pytest.fixture(autouse=True) @@ -1637,3 +1644,282 @@ def test_create_stage_raises_insufficient_privileges_error( sql_facade.create_stage(stage, role=role, database=database) mock_execute_query.assert_has_calls(expected) + + +def test_upgrade_application_unversioned(mock_execute_query, mock_cursor): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + + 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 + + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + ) + + mock_execute_query.assert_has_calls(expected) + + +def test_upgrade_application_version_and_patch(mock_execute_query, mock_cursor): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call( + f"alter application {app_name} upgrade using version 3 patch 2" + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.versioned_dev("3", 2), + stage_fqn=stage_fqn, + ) + + mock_execute_query.assert_has_calls(expected) + + +def test_upgrade_application_from_release_directive(mock_execute_query, mock_cursor): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call(f"alter application {app_name} upgrade "), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + ) + + mock_execute_query.assert_has_calls(expected) + + +def test_upgrade_application_raises_event_sharing_error(mock_execute_query): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError(errno=APPLICATION_REQUIRES_TELEMETRY_SHARING), + mock.call(f"alter application {app_name} upgrade using @{stage_fqn}"), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + with pytest.raises(ProgrammingError): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + ) + + mock_execute_query.assert_has_calls(expected) + + +def test_upgrade_application_converts_other_programmingerrors(mock_execute_query): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + 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 + + with pytest.raises(UserInputError) as err: + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + ) + + assert err.match( + f"Failed to upgrade application {app_name} with the following error message:\n" + ) + assert err.match(programming_error_message) + + mock_execute_query.assert_has_calls(expected) + + +def test_create_application_with_minimal_clauses(mock_execute_query, mock_cursor): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + + 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 + + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + debug_mode=None, + new_authorize_event_sharing_value=None, + ) + + mock_execute_query.assert_has_calls(expected) + + +def test_create_application_with_all_clauses(mock_execute_query, mock_cursor): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + + 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 + + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + debug_mode=True, + new_authorize_event_sharing_value=True, + ) + + mock_execute_query.assert_has_calls(expected) + + +def test_create_application_raises_event_sharing_error(mock_execute_query): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + + 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} + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + with pytest.raises(ProgrammingError): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + debug_mode=None, + new_authorize_event_sharing_value=None, + ) + + mock_execute_query.assert_has_calls(expected) + + +def test_create_application_converts_other_programmingerrors(mock_execute_query): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + 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 + + with 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, + new_authorize_event_sharing_value=None, + ) + + assert err.match( + f"Failed to create application {app_name} with the following error message:\n" + ) + assert err.match(programming_error_message) + + mock_execute_query.assert_has_calls(expected) From 4f48b11da3cf51bc96bc93b333f1a796a23890d2 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Mon, 18 Nov 2024 21:16:18 -0500 Subject: [PATCH 02/24] migrate all tests to sql facade for create/upgrade --- .../nativeapp/entities/application.py | 32 +- .../nativeapp/same_account_install_method.py | 22 +- .../cli/_plugins/nativeapp/sf_sql_facade.py | 15 +- src/snowflake/cli/api/entities/utils.py | 2 +- tests/nativeapp/test_run_processor.py | 604 +++++++++++------- tests/nativeapp/test_sf_sql_facade.py | 70 +- tests/nativeapp/utils.py | 11 + 7 files changed, 423 insertions(+), 333 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index c4f653afe5..97010e35e7 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -52,6 +52,7 @@ SameAccountInstallMethod, ) from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade +from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import UserInputError 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 @@ -725,19 +726,28 @@ def create_or_upgrade_app( self.execute_post_deploy_hooks() return - except ProgrammingError as err: - if err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY: + except (UserInputError, ProgrammingError) as err: + is_programming_error = isinstance(err, ProgrammingError) + errno = ( + err.errno if is_programming_error else err.__cause__.errno + ) + + if 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) + elif errno in UPGRADE_RESTRICTION_CODES: + console.warning( + err.msg if is_programming_error else err.message + ) self.drop_application_before_upgrade( policy=policy, interactive=interactive ) - else: + elif is_programming_error: generic_sql_error_handler(err=err) + else: + raise 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.") @@ -777,13 +787,19 @@ def create_or_upgrade_app( # hooks always executed after a create or upgrade self.execute_post_deploy_hooks() - except ProgrammingError as err: - if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING: + except (UserInputError, ProgrammingError) as err: + is_programming_error = isinstance(err, ProgrammingError) + errno = err.errno if is_programming_error else err.__cause__.errno + + if 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) + elif is_programming_error: + generic_sql_error_handler(err) + else: + raise err def execute_post_deploy_hooks(self): execute_post_deploy_hooks( 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..1aa96946da 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 ( @@ -10,23 +11,12 @@ from snowflake.cli._plugins.stage.manager import StageManager +@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 +29,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: diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index 509acab723..098b63c92e 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -32,7 +32,6 @@ handle_unclassified_error, ) from snowflake.cli.api.errno import ( - APPLICATION_REQUIRES_TELEMETRY_SHARING, DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, @@ -511,7 +510,7 @@ def show_release_directives( def upgrade_application( self, name: str, install_method: SameAccountInstallMethod, stage_fqn: str - ): + ) -> list[tuple[str]]: """ Upgrades an application object using the provided clause """ @@ -520,17 +519,14 @@ def upgrade_application( upgrade_cursor = self._sql_executor.execute_query( f"alter application {name} upgrade {using_clause}", ) - return upgrade_cursor.fetchall() except ProgrammingError as err: - if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING: - # this needs to be bubbled up so that it can be handled by the EventSharingHandler - raise raise UserInputError( f"Failed to upgrade application {name} with the following error message:\n" f"{err.msg}" ) from err except Exception as err: handle_unclassified_error(err, f"Failed to upgrade application {name}.") + return upgrade_cursor.fetchall() def create_application( self, @@ -540,7 +536,7 @@ def create_application( stage_fqn: str, debug_mode: bool | None, new_authorize_event_sharing_value: bool | None, - ): + ) -> list[tuple[str]]: """ Creates a new application object using an application package, running the setup script of the application package @@ -572,17 +568,14 @@ def create_application( """ ), ) - return create_cursor.fetchall() except ProgrammingError as err: - if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING: - # this needs to be bubbled up so that it can be handled by the EventSharingHandler - raise raise UserInputError( f"Failed to create application {name} with the following error message:\n" f"{err.msg}" ) from err 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 diff --git a/src/snowflake/cli/api/entities/utils.py b/src/snowflake/cli/api/entities/utils.py index 9507d5b689..400fe726f4 100644 --- a/src/snowflake/cli/api/entities/utils.py +++ b/src/snowflake/cli/api/entities/utils.py @@ -324,7 +324,7 @@ def drop_generic_object( console.message(f"Dropped {object_type} {object_name} successfully.") -def print_messages(console: AbstractConsole, cursor_results: list): +def print_messages(console: AbstractConsole, cursor_results: list[tuple[str]]): """ Shows messages in the console returned by the CREATE or UPGRADE APPLICATION command. diff --git a/tests/nativeapp/test_run_processor.py b/tests/nativeapp/test_run_processor.py index 9a91c9f88b..1f18422481 100644 --- a/tests/nativeapp/test_run_processor.py +++ b/tests/nativeapp/test_run_processor.py @@ -46,6 +46,7 @@ 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.stage.diff import DiffResult from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext from snowflake.cli._plugins.workspace.manager import WorkspaceManager @@ -58,10 +59,10 @@ APPLICATION_OWNS_EXTERNAL_OBJECTS, CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, INSUFFICIENT_PRIVILEGES, + NO_WAREHOUSE_SELECTED_IN_SESSION, ) from snowflake.cli.api.exceptions import ( CouldNotUseObjectError, - NoWarehouseSelectedInSessionError, ) from snowflake.cli.api.project.definition_manager import DefinitionManager from snowflake.connector import ProgrammingError @@ -75,8 +76,11 @@ APP_PACKAGE_ENTITY_GET_EXISTING_VERSION_INFO, GET_UI_PARAMETERS, SQL_EXECUTOR_EXECUTE, + SQL_FACADE_CREATE_APPLICATION, + 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 +117,20 @@ 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." + + 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() @@ -258,6 +270,7 @@ def test_create_dev_app_w_warehouse_access_exception( # 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_FACADE_CREATE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -271,6 +284,7 @@ def test_create_dev_app_create_new_w_no_additional_privileges( mock_param, mock_conn, mock_execute, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -287,18 +301,6 @@ def test_create_dev_app_create_new_w_no_additional_privileges( 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()"), @@ -316,6 +318,9 @@ def test_create_dev_app_create_new_w_no_additional_privileges( ) 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() @@ -327,6 +332,16 @@ def test_create_dev_app_create_new_w_no_additional_privileges( 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, + new_authorize_event_sharing_value=None, + ) + ] # Test create_dev_app with no existing application AND create returns a warning @@ -464,6 +479,7 @@ def test_create_or_upgrade_dev_app_with_warning( # 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_FACADE_CREATE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -477,6 +493,7 @@ def test_create_dev_app_create_new_with_additional_privileges( mock_param, mock_conn, mock_execute_query, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -513,18 +530,6 @@ def test_create_dev_app_create_new_with_additional_privileges( 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()"), @@ -542,6 +547,9 @@ def test_create_dev_app_create_new_with_additional_privileges( ) 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()) @@ -551,10 +559,21 @@ def test_create_dev_app_create_new_with_additional_privileges( 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, + new_authorize_event_sharing_value=None, + ) + ] # 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_FACADE_CREATE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -568,6 +587,7 @@ 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, @@ -584,20 +604,6 @@ def test_create_dev_app_create_new_w_missing_warehouse_exception( 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")), ] @@ -605,20 +611,34 @@ def test_create_dev_app_create_new_w_missing_warehouse_exception( 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 err.match("No active warehouse selected in the current session") 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, + new_authorize_event_sharing_value=None, + ) + ] # Test create_dev_app with existing application AND bad comment AND good version @@ -690,6 +710,7 @@ def test_create_dev_app_incorrect_properties( # Test create_dev_app with existing application AND incorrect owner @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -703,6 +724,7 @@ 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, @@ -725,26 +747,24 @@ def test_create_dev_app_incorrect_owner( 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(), @@ -752,10 +772,18 @@ def test_create_dev_app_incorrect_owner( ) 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, + ) + ] # Test create_dev_app with existing application AND diff has no changes @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch( GET_UI_PARAMETERS, @@ -769,6 +797,7 @@ def test_create_dev_app_no_diff_changes( mock_param, mock_conn, mock_execute, + mock_sql_facade_upgrade_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -791,12 +820,6 @@ def test_create_dev_app_no_diff_changes( 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()"), @@ -826,6 +849,9 @@ def test_create_dev_app_no_diff_changes( ) 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()) @@ -835,10 +861,18 @@ def test_create_dev_app_no_diff_changes( 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, + ) + ] # Test create_dev_app with existing application AND diff has changes @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -852,6 +886,7 @@ def test_create_dev_app_w_diff_changes( mock_param, mock_conn, mock_execute, + mock_sql_facade_upgrade_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -874,12 +909,6 @@ def test_create_dev_app_w_diff_changes( 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()"), @@ -909,6 +938,9 @@ def test_create_dev_app_w_diff_changes( ) 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()) @@ -918,10 +950,18 @@ def test_create_dev_app_w_diff_changes( 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, + ) + ] # Test create_dev_app with existing application AND alter throws an error @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -935,6 +975,7 @@ 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, @@ -957,38 +998,35 @@ def test_create_dev_app_recreate_w_missing_warehouse_exception( 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_FACADE_CREATE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1002,6 +1040,7 @@ def test_create_dev_app_create_new_quoted( mock_param, mock_conn, mock_execute, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -1018,18 +1057,6 @@ def test_create_dev_app_create_new_quoted( 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()"), @@ -1047,6 +1074,9 @@ def test_create_dev_app_create_new_quoted( ) 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( @@ -1085,10 +1115,21 @@ def test_create_dev_app_create_new_quoted( 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, + new_authorize_event_sharing_value=None, + ) + ] # 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_FACADE_CREATE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1102,6 +1143,7 @@ def test_create_dev_app_create_new_quoted_override( mock_param, mock_conn, mock_execute, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -1118,18 +1160,6 @@ def test_create_dev_app_create_new_quoted_override( 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()"), @@ -1147,6 +1177,9 @@ def test_create_dev_app_create_new_quoted_override( ) 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() @@ -1164,6 +1197,16 @@ def test_create_dev_app_create_new_quoted_override( 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, + new_authorize_event_sharing_value=None, + ) + ] # Test run existing app info @@ -1171,6 +1214,8 @@ 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_EXECUTOR_EXECUTE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock_connection() @@ -1186,6 +1231,8 @@ def test_create_dev_app_recreate_app_when_orphaned( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1207,12 +1254,6 @@ def test_create_dev_app_recreate_app_when_orphaned( 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",)], []), @@ -1234,18 +1275,6 @@ def test_create_dev_app_recreate_app_when_orphaned( 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()"), @@ -1264,12 +1293,38 @@ def test_create_dev_app_recreate_app_when_orphaned( 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=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, + ) + ] + 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, + new_authorize_event_sharing_value=None, + ) + ] # Test run existing app info @@ -1278,6 +1333,8 @@ 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_EXECUTOR_EXECUTE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock_connection() @@ -1293,6 +1350,8 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1315,12 +1374,6 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( 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"), @@ -1359,18 +1412,6 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( 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()"), @@ -1388,6 +1429,13 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( ) 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=APPLICATION_NO_LONGER_AVAILABLE), + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -1395,6 +1443,23 @@ 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, + ) + ] + 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, + new_authorize_event_sharing_value=None, + ) + ] # Test run existing app info @@ -1404,6 +1469,8 @@ 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_EXECUTOR_EXECUTE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock_connection() @@ -1419,6 +1486,8 @@ 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_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1440,12 +1509,6 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje 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"), @@ -1479,18 +1542,6 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje 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()"), @@ -1508,6 +1559,12 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = 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,6 +1572,23 @@ 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, + ) + ] + 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, + new_authorize_event_sharing_value=None, + ) + ] # Test upgrade app method for release directives AND throws warehouse error @@ -1572,6 +1646,7 @@ 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_connection() @@ -1590,6 +1665,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, @@ -1611,32 +1687,40 @@ def test_upgrade_app_incorrect_owner( 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(), ) + err.match("Insufficient privileges to operate on database") 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, + ) + ] # Test upgrade app method for release directives AND existing app info AND upgrade succeeds +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock_connection() @@ -1655,6 +1739,7 @@ def test_upgrade_app_succeeds( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, policy_param, temp_dir, mock_cursor, @@ -1676,7 +1761,6 @@ def test_upgrade_app_succeeds( 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()"), @@ -1705,6 +1789,9 @@ def test_upgrade_app_succeeds( ) 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()) @@ -1714,9 +1801,17 @@ def test_upgrade_app_succeeds( 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, + ) + ] # Test upgrade app method for release directives AND existing app info AND upgrade fails due to generic error +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock_connection() @@ -1735,6 +1830,7 @@ def test_upgrade_app_fails_generic_error( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, policy_param, temp_dir, mock_cursor, @@ -1756,28 +1852,35 @@ def test_upgrade_app_fails_generic_error( 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, + ) + ] # 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 @@ -1854,6 +1957,8 @@ def test_upgrade_app_fails_upgrade_restriction_error( assert mock_execute.mock_calls == expected +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock_connection() @@ -1869,6 +1974,8 @@ def test_versioned_app_upgrade_to_unversioned( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1894,15 +2001,6 @@ def test_versioned_app_upgrade_to_unversioned( 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",)], []), @@ -1924,18 +2022,6 @@ def test_versioned_app_upgrade_to_unversioned( 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()"), @@ -1953,6 +2039,16 @@ def test_versioned_app_upgrade_to_unversioned( ) 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="Some Error Message.", + errno=93045, + ), + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -1962,11 +2058,29 @@ def test_versioned_app_upgrade_to_unversioned( 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, + ) + ] + 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, + new_authorize_event_sharing_value=None, + ) + ] # 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( @@ -1990,6 +2104,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, @@ -2012,12 +2127,6 @@ def test_upgrade_app_fails_drop_fails( 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, @@ -2030,6 +2139,9 @@ def test_upgrade_app_fails_drop_fails( ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = ProgrammingError( + errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + ) setup_project_file(os.getcwd()) @@ -2040,9 +2152,18 @@ 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, + ) + ] # 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_EXECUTOR_EXECUTE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock.patch( @@ -2063,6 +2184,8 @@ def test_upgrade_app_recreate_app( mock_typer_confirm, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, policy_param, temp_dir, mock_cursor, @@ -2084,12 +2207,6 @@ def test_upgrade_app_recreate_app( 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",)], []), @@ -2111,18 +2228,6 @@ def test_upgrade_app_recreate_app( 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()"), @@ -2140,6 +2245,12 @@ def test_upgrade_app_recreate_app( ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = 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 +2260,23 @@ 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, + ) + ] + 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, + new_authorize_event_sharing_value=None, + ) + ] # Test upgrade app method for version AND no existing version info @@ -2203,6 +2331,8 @@ 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_EXECUTOR_EXECUTE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock.patch( @@ -2223,6 +2353,8 @@ def test_upgrade_app_recreate_app_from_version( mock_typer_confirm, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_existing, policy_param, temp_dir, @@ -2246,12 +2378,6 @@ def test_upgrade_app_recreate_app_from_version( 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",)], []), @@ -2273,18 +2399,6 @@ def test_upgrade_app_recreate_app_from_version( 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()"), @@ -2302,6 +2416,15 @@ def test_upgrade_app_recreate_app_from_version( ) 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=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()) @@ -2321,6 +2444,23 @@ 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, + ) + ] + 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, + new_authorize_event_sharing_value=None, + ) + ] # 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 006a416319..46b7e0ab32 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -1717,31 +1717,7 @@ def test_upgrade_application_from_release_directive(mock_execute_query, mock_cur mock_execute_query.assert_has_calls(expected) -def test_upgrade_application_raises_event_sharing_error(mock_execute_query): - app_name = "test_app" - stage_fqn = "app_pkg.app_src.stage" - - side_effects, expected = mock_execute_helper( - [ - ( - ProgrammingError(errno=APPLICATION_REQUIRES_TELEMETRY_SHARING), - mock.call(f"alter application {app_name} upgrade using @{stage_fqn}"), - ) - ] - ) - mock_execute_query.side_effect = side_effects - - with pytest.raises(ProgrammingError): - sql_facade.upgrade_application( - name=app_name, - install_method=SameAccountInstallMethod.unversioned_dev(), - stage_fqn=stage_fqn, - ) - - mock_execute_query.assert_has_calls(expected) - - -def test_upgrade_application_converts_other_programmingerrors(mock_execute_query): +def test_upgrade_application_converts_programmingerrors(mock_execute_query): app_name = "test_app" stage_fqn = "app_pkg.app_src.stage" programming_error_message = "programming error message" @@ -1750,8 +1726,8 @@ def test_upgrade_application_converts_other_programmingerrors(mock_execute_query [ ( ProgrammingError( - errno=APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, - msg="programming error message", + errno=APPLICATION_REQUIRES_TELEMETRY_SHARING, + msg=programming_error_message, ), mock.call(f"alter application {app_name} upgrade using @{stage_fqn}"), ) @@ -1844,43 +1820,7 @@ def test_create_application_with_all_clauses(mock_execute_query, mock_cursor): mock_execute_query.assert_has_calls(expected) -def test_create_application_raises_event_sharing_error(mock_execute_query): - app_name = "test_app" - pkg_name = "test_pkg" - stage_fqn = "app_pkg.app_src.stage" - - 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} - comment = {SPECIAL_COMMENT} - """ - ) - ), - ) - ] - ) - mock_execute_query.side_effect = side_effects - - with pytest.raises(ProgrammingError): - sql_facade.create_application( - name=app_name, - package_name=pkg_name, - install_method=SameAccountInstallMethod.release_directive(), - stage_fqn=stage_fqn, - debug_mode=None, - new_authorize_event_sharing_value=None, - ) - - mock_execute_query.assert_has_calls(expected) - - -def test_create_application_converts_other_programmingerrors(mock_execute_query): +def test_create_application_converts_programmingerrors(mock_execute_query): app_name = "test_app" pkg_name = "test_pkg" stage_fqn = "app_pkg.app_src.stage" @@ -1891,7 +1831,7 @@ def test_create_application_converts_other_programmingerrors(mock_execute_query) ( ProgrammingError( errno=APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, - msg="programming error message", + msg=programming_error_message, ), mock.call( dedent( diff --git a/tests/nativeapp/utils.py b/tests/nativeapp/utils.py index 76dd46d9ec..ddcc25f509 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -19,6 +19,8 @@ from textwrap import dedent from typing import List, Set +import pytest + from tests.nativeapp.factories import ProjectV10Factory TYPER_CONFIRM = "typer.confirm" @@ -73,6 +75,8 @@ 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" mock_snowflake_yml_file = dedent( """\ @@ -308,3 +312,10 @@ 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 From 5c0d52d56e1cf963b1b799dc2f78b5dc473708f7 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Wed, 20 Nov 2024 10:14:15 -0500 Subject: [PATCH 03/24] update event sharing tests for refactor --- tests/nativeapp/test_event_sharing.py | 357 +++++++++++++++++++++----- 1 file changed, 288 insertions(+), 69 deletions(-) diff --git a/tests/nativeapp/test_event_sharing.py b/tests/nativeapp/test_event_sharing.py index c26285f9b8..a3faa9cc7d 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,6 +36,7 @@ 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 @@ -62,10 +60,20 @@ APP_ENTITY_GET_EXISTING_APP_INFO, GET_UI_PARAMETERS, SQL_EXECUTOR_EXECUTE, + SQL_FACADE_CREATE_APPLICATION, + 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_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." + allow_always_policy = AllowAlwaysPolicy() ask_always_policy = AskAlwaysPolicy() deny_always_policy = DenyAlwaysPolicy() @@ -187,6 +195,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, @@ -200,6 +210,7 @@ def _setup_mocks_for_app( ): if is_upgrade: return _setup_mocks_for_upgrade_app( + mock_sql_facade_upgrade_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -212,6 +223,7 @@ def _setup_mocks_for_app( ) else: return _setup_mocks_for_create_app( + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -224,6 +236,7 @@ def _setup_mocks_for_app( def _setup_mocks_for_create_app( + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -235,13 +248,6 @@ def _setup_mocks_for_create_app( ): 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",)], []), @@ -273,18 +279,6 @@ def _setup_mocks_for_create_app( 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()"), @@ -318,10 +312,33 @@ def _setup_mocks_for_create_app( ) 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 = ( + mock_side_effect_error_with_cause( + err=UserInputError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=programming_errno), + ) + if programming_errno + else mock_cursor([[(DEFAULT_CREATE_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, + new_authorize_event_sharing_value=expected_authorize_telemetry_flag, + ) + ] + + return [*mock_execute_query_expected, *mock_sql_facade_create_application_expected] def _setup_mocks_for_upgrade_app( + mock_sql_facade_upgrade_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -335,9 +352,6 @@ def _setup_mocks_for_upgrade_app( mock_get_existing_app_info.return_value = { "comment": "GENERATED_BY_SNOWFLAKECLI", } - install_clause = "using @app_pkg.app_src.stage" - if is_prod: - install_clause = "" calls = [ ( @@ -350,7 +364,6 @@ def _setup_mocks_for_upgrade_app( 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()"), @@ -417,10 +430,25 @@ def _setup_mocks_for_upgrade_app( ) 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 = mock_cursor( + [[(DEFAULT_UPGRADE_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, + ) + ] + return [*mock_execute_query_expected, *mock_sql_facade_upgrade_application_expected] @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -459,6 +487,8 @@ def test_event_sharing_disabled_no_change_to_current_behavior( mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -467,7 +497,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 +519,22 @@ 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, + ] == expected + + mock_console.warning.assert_called_once_with( + DEFAULT_UPGRADE_SUCCESS_MESSAGE + if is_upgrade + else DEFAULT_CREATE_SUCCESS_MESSAGE + ) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -521,6 +564,8 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -529,7 +574,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 +597,27 @@ 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, + ] == 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_UPGRADE_SUCCESS_MESSAGE + if is_upgrade + else DEFAULT_CREATE_SUCCESS_MESSAGE + ), + ] @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -585,6 +646,8 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -593,7 +656,9 @@ 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, @@ -616,11 +681,22 @@ 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, + ] == expected + + mock_console.warning.assert_called_once_with( + DEFAULT_UPGRADE_SUCCESS_MESSAGE + if is_upgrade + else DEFAULT_CREATE_SUCCESS_MESSAGE + ) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -649,6 +725,8 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -657,7 +735,9 @@ 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, @@ -680,11 +760,18 @@ 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, + ] == expected + + mock_console.warning.assert_called_once_with(DEFAULT_UPGRADE_SUCCESS_MESSAGE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -711,6 +798,8 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -719,7 +808,9 @@ 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, @@ -750,11 +841,22 @@ 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 [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + ] == expected + + mock_console.warning.assert_called_once_with( + DEFAULT_UPGRADE_SUCCESS_MESSAGE + if is_upgrade + else DEFAULT_CREATE_SUCCESS_MESSAGE + ) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -781,6 +883,8 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -789,7 +893,9 @@ 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, @@ -822,14 +928,27 @@ 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, + ] == expected + + assert mock_console.warning.mock_calls == [ + mock.call( + DEFAULT_UPGRADE_SUCCESS_MESSAGE + if is_upgrade + else DEFAULT_CREATE_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_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -856,6 +975,8 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -864,7 +985,9 @@ 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, @@ -887,11 +1010,22 @@ 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, + ] == expected + + mock_console.warning.assert_called_once_with( + DEFAULT_UPGRADE_SUCCESS_MESSAGE + if is_upgrade + else DEFAULT_CREATE_SUCCESS_MESSAGE + ) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -918,6 +1052,8 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -926,7 +1062,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 +1086,22 @@ 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, + ] == expected + + mock_console.warning.assert_called_once_with( + DEFAULT_UPGRADE_SUCCESS_MESSAGE + if is_upgrade + else DEFAULT_CREATE_SUCCESS_MESSAGE + ) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -979,6 +1128,8 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -988,6 +1139,8 @@ 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, @@ -1027,6 +1180,8 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1053,6 +1208,8 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1061,7 +1218,9 @@ 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, @@ -1097,10 +1256,12 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio e.value.message == "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." ) - mock_console.warning.assert_not_called() + mock_console.warning.assert_called_once_with(DEFAULT_UPGRADE_SUCCESS_MESSAGE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1126,6 +1287,8 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1134,7 +1297,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 +1321,25 @@ 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, + ] == 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_UPGRADE_SUCCESS_MESSAGE + if is_upgrade + else DEFAULT_CREATE_SUCCESS_MESSAGE + ), + ] @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1187,6 +1365,8 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1195,7 +1375,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, @@ -1234,6 +1416,8 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1259,6 +1443,8 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1267,7 +1453,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 +1484,22 @@ 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, + ] == expected + + mock_console.warning.assert_called_once_with( + DEFAULT_UPGRADE_SUCCESS_MESSAGE + if is_upgrade + else DEFAULT_CREATE_SUCCESS_MESSAGE + ) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1325,6 +1525,8 @@ def test_shared_events_with_no_enabled_mandatory_events_then_error( mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1333,7 +1535,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, @@ -1364,6 +1568,8 @@ def test_shared_events_with_no_enabled_mandatory_events_then_error( @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -1390,6 +1596,8 @@ def test_shared_events_with_authorization_then_success( mock_param, mock_conn, mock_execute_query, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1399,7 +1607,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 +1646,14 @@ 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, + ] == expected + + mock_console.warning.assert_called_once_with( + DEFAULT_UPGRADE_SUCCESS_MESSAGE + if is_upgrade + else DEFAULT_CREATE_SUCCESS_MESSAGE + ) From 6595561025379f2c677a9d184197e15fcaf1743e Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Thu, 21 Nov 2024 09:54:37 -0500 Subject: [PATCH 04/24] merge success messages for tests --- tests/nativeapp/test_event_sharing.py | 71 ++++++--------------------- 1 file changed, 15 insertions(+), 56 deletions(-) diff --git a/tests/nativeapp/test_event_sharing.py b/tests/nativeapp/test_event_sharing.py index a3faa9cc7d..a7abb2ee0d 100644 --- a/tests/nativeapp/test_event_sharing.py +++ b/tests/nativeapp/test_event_sharing.py @@ -70,8 +70,7 @@ 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_SUCCESS_MESSAGE = "Application successfully upgraded." DEFAULT_USER_INPUT_ERROR_MESSAGE = "User input error message." allow_always_policy = AllowAlwaysPolicy() @@ -319,7 +318,7 @@ def _setup_mocks_for_create_app( cause=ProgrammingError(errno=programming_errno), ) if programming_errno - else mock_cursor([[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], []) + else mock_cursor([[(DEFAULT_SUCCESS_MESSAGE,)]], []) ) mock_sql_facade_create_application_expected = [ mock.call( @@ -432,7 +431,7 @@ def _setup_mocks_for_upgrade_app( mock_execute_query.side_effect = side_effects mock_sql_facade_upgrade_application.side_effect = mock_cursor( - [[(DEFAULT_UPGRADE_SUCCESS_MESSAGE,)]], [] + [[(DEFAULT_SUCCESS_MESSAGE,)]], [] ) mock_sql_facade_upgrade_application_expected = [ mock.call( @@ -525,11 +524,7 @@ def test_event_sharing_disabled_no_change_to_current_behavior( *mock_sql_facade_create_application.mock_calls, ] == expected - mock_console.warning.assert_called_once_with( - DEFAULT_UPGRADE_SUCCESS_MESSAGE - if is_upgrade - else DEFAULT_CREATE_SUCCESS_MESSAGE - ) + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @@ -607,11 +602,7 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit mock.call( "WARNING: Same-account event sharing is not enabled in your account, therefore, application telemetry section will be ignored." ), - mock.call( - DEFAULT_UPGRADE_SUCCESS_MESSAGE - if is_upgrade - else DEFAULT_CREATE_SUCCESS_MESSAGE - ), + mock.call(DEFAULT_SUCCESS_MESSAGE), ] @@ -687,11 +678,7 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec *mock_sql_facade_create_application.mock_calls, ] == expected - mock_console.warning.assert_called_once_with( - DEFAULT_UPGRADE_SUCCESS_MESSAGE - if is_upgrade - else DEFAULT_CREATE_SUCCESS_MESSAGE - ) + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) @@ -766,7 +753,7 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no *mock_sql_facade_create_application.mock_calls, ] == expected - mock_console.warning.assert_called_once_with(DEFAULT_UPGRADE_SUCCESS_MESSAGE) + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) @@ -847,11 +834,7 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ *mock_sql_facade_create_application.mock_calls, ] == expected - mock_console.warning.assert_called_once_with( - DEFAULT_UPGRADE_SUCCESS_MESSAGE - if is_upgrade - else DEFAULT_CREATE_SUCCESS_MESSAGE - ) + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) @@ -935,11 +918,7 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f ] == expected assert mock_console.warning.mock_calls == [ - mock.call( - DEFAULT_UPGRADE_SUCCESS_MESSAGE - if is_upgrade - else DEFAULT_CREATE_SUCCESS_MESSAGE - ), + 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." ), @@ -1016,11 +995,7 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide *mock_sql_facade_create_application.mock_calls, ] == expected - mock_console.warning.assert_called_once_with( - DEFAULT_UPGRADE_SUCCESS_MESSAGE - if is_upgrade - else DEFAULT_CREATE_SUCCESS_MESSAGE - ) + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) @@ -1092,11 +1067,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide *mock_sql_facade_upgrade_application.mock_calls, ] == expected - mock_console.warning.assert_called_once_with( - DEFAULT_UPGRADE_SUCCESS_MESSAGE - if is_upgrade - else DEFAULT_CREATE_SUCCESS_MESSAGE - ) + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) @@ -1256,7 +1227,7 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio e.value.message == "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." ) - mock_console.warning.assert_called_once_with(DEFAULT_UPGRADE_SUCCESS_MESSAGE) + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) @@ -1329,11 +1300,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default 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." assert mock_console.warning.mock_calls == [ mock.call(expected_warning), - mock.call( - DEFAULT_UPGRADE_SUCCESS_MESSAGE - if is_upgrade - else DEFAULT_CREATE_SUCCESS_MESSAGE - ), + mock.call(DEFAULT_SUCCESS_MESSAGE), ] @@ -1490,11 +1457,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe *mock_sql_facade_create_application.mock_calls, ] == expected - mock_console.warning.assert_called_once_with( - DEFAULT_UPGRADE_SUCCESS_MESSAGE - if is_upgrade - else DEFAULT_CREATE_SUCCESS_MESSAGE - ) + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) @@ -1652,8 +1615,4 @@ def test_shared_events_with_authorization_then_success( *mock_sql_facade_create_application.mock_calls, ] == expected - mock_console.warning.assert_called_once_with( - DEFAULT_UPGRADE_SUCCESS_MESSAGE - if is_upgrade - else DEFAULT_CREATE_SUCCESS_MESSAGE - ) + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) From a71f4a93df01e03f9221e4319626f715f94fe8ab Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Thu, 21 Nov 2024 10:30:39 -0500 Subject: [PATCH 05/24] move error handling to facade --- .../nativeapp/entities/application.py | 62 ++++--------------- .../nativeapp/sf_facade_exceptions.py | 9 +++ .../cli/_plugins/nativeapp/sf_sql_facade.py | 37 +++++++++++ 3 files changed, 57 insertions(+), 51 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index 97010e35e7..fc6c421b1c 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -52,7 +52,9 @@ SameAccountInstallMethod, ) from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade -from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import UserInputError +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 @@ -71,13 +73,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 +94,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}) @@ -725,29 +712,13 @@ def create_or_upgrade_app( # hooks always executed after a create or upgrade self.execute_post_deploy_hooks() return - - except (UserInputError, ProgrammingError) as err: - is_programming_error = isinstance(err, ProgrammingError) - errno = ( - err.errno if is_programming_error else err.__cause__.errno + except UpgradeApplicationRestrictionError as err: + console.warning(err.message) + self.drop_application_before_upgrade( + policy=policy, interactive=interactive ) - - if 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 errno in UPGRADE_RESTRICTION_CODES: - console.warning( - err.msg if is_programming_error else err.message - ) - self.drop_application_before_upgrade( - policy=policy, interactive=interactive - ) - elif is_programming_error: - generic_sql_error_handler(err=err) - else: - raise err + except ProgrammingError as err: + 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.") @@ -787,19 +758,8 @@ def create_or_upgrade_app( # hooks always executed after a create or upgrade self.execute_post_deploy_hooks() - except (UserInputError, ProgrammingError) as err: - is_programming_error = isinstance(err, ProgrammingError) - errno = err.errno if is_programming_error else err.__cause__.errno - - if 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, - ) - elif is_programming_error: - generic_sql_error_handler(err) - else: - raise err + except ProgrammingError as err: + generic_sql_error_handler(err) def execute_post_deploy_hooks(self): execute_post_deploy_hooks( diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py index 1b0d7c722c..f899308992 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py @@ -115,3 +115,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 098b63c92e..3207f7d745 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -27,16 +27,26 @@ CouldNotUseObjectError, InsufficientPrivilegesError, UnexpectedResultError, + UpgradeApplicationRestrictionError, UserInputError, UserScriptError, handle_unclassified_error, ) +from snowflake.cli.api.cli_global_context import get_cli_context from snowflake.cli.api.errno import ( + APPLICATION_NO_LONGER_AVAILABLE, + 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_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, + NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, ) 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, @@ -47,6 +57,15 @@ from snowflake.cli.api.sql_execution import BaseSqlExecutor, SqlExecutor from snowflake.connector import DictCursor, 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, +} + class SnowflakeSQLFacade: def __init__(self, sql_executor: SqlExecutor | None = None): @@ -520,6 +539,16 @@ def upgrade_application( f"alter application {name} upgrade {using_clause}", ) except ProgrammingError as err: + if err.errno in UPGRADE_RESTRICTION_CODES: + raise UpgradeApplicationRestrictionError(err.msg) from err + elif 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 + raise UserInputError( f"Failed to upgrade application {name} with the following error message:\n" f"{err.msg}" @@ -569,6 +598,14 @@ def create_application( ), ) 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 + raise UserInputError( f"Failed to create application {name} with the following error message:\n" f"{err.msg}" From f1f00f00de96e8a657122424b5f5bb0f998ea79c Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Fri, 22 Nov 2024 17:40:30 -0500 Subject: [PATCH 06/24] further overhauls to create/upgrade to make uniform, fix version not proper identifier when number bug, pending event sharing unit tests --- .../nativeapp/entities/application.py | 242 +++++------- .../nativeapp/same_account_install_method.py | 4 +- .../cli/_plugins/nativeapp/sf_sql_facade.py | 150 ++++--- tests/nativeapp/test_run_processor.py | 373 +++++++++++------- tests/nativeapp/test_sf_sql_facade.py | 253 +++++++++++- tests/nativeapp/utils.py | 3 + 6 files changed, 666 insertions(+), 359 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index fc6c421b1c..fadb123bcb 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -26,7 +26,6 @@ ) from snowflake.cli._plugins.nativeapp.constants import ( ALLOWED_SPECIAL_COMMENTS, - AUTHORIZE_TELEMETRY_COL, COMMENT_COL, NAME_COL, OWNER_COL, @@ -178,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. @@ -195,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. @@ -621,6 +591,75 @@ def get_objects_owned_by_application(self) -> List[ApplicationOwnedObject]: ).fetchall() return [{"name": row[1], "type": row[2]} for row in results] + def _upgrade_app( + self, + stage_fqn: str, + install_method: SameAccountInstallMethod, + event_sharing: EventSharingHandler, + current_app_row: dict, + policy: PolicyBase, + interactive: bool, + ) -> list[tuple[str]] | None: + console = self._workspace_ctx.console + self._workspace_ctx.console.step( + f"Upgrading existing application object {self.name}." + ) + + try: + return get_snowflake_facade().upgrade_application( + name=self.name, + current_app_row=current_app_row, + install_method=install_method, + stage_fqn=stage_fqn, + debug_mode=self._entity_model.debug, + should_authorize_event_sharing=event_sharing.should_authorize_event_sharing(), + role=self.role, + warehouse=self.warehouse, + ) + except UpgradeApplicationRestrictionError as err: + console.warning(err.message) + self.drop_application_before_upgrade(policy=policy, interactive=interactive) + return None + + def _create_app( + self, + stage_fqn: str, + install_method: SameAccountInstallMethod, + event_sharing: EventSharingHandler, + package: ApplicationPackageEntity, + ) -> list[tuple[str]]: + self._workspace_ctx.console.step( + f"Creating new application object {self.name} in account." + ) + + try: + sql_executor = get_sql_executor() + 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}" + ) + stage_schema = extract_schema(stage_fqn) + 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}" + ) + except ProgrammingError as err: + generic_sql_error_handler(err) + + return get_snowflake_facade().create_application( + name=self.name, + package_name=package.name, + install_method=install_method, + stage_fqn=stage_fqn, + debug_mode=self._entity_model.debug, + should_authorize_event_sharing=event_sharing.should_authorize_event_sharing(), + role=self.role, + warehouse=self.warehouse, + ) + @span("update_app_object") def create_or_upgrade_app( self, @@ -630,20 +669,14 @@ def create_or_upgrade_app( policy: PolicyBase, interactive: bool, ): - model = self._entity_model - console = self._workspace_ctx.console - debug_mode = model.debug - - 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, + telemetry_definition=self._entity_model.telemetry, deploy_root=package.deploy_root, install_method=install_method, - console=console, + console=self._workspace_ctx.console, ) # 1. Need to use a warehouse to create an application object @@ -652,114 +685,43 @@ def create_or_upgrade_app( # 2. Check for an existing application by the same name show_app_row = self.get_existing_app_info() + stage_fqn = stage_fqn or package.stage_fqn + # 3. If existing application is found, perform a few validations and upgrade the application object. + create_or_upgrade_result = None if show_app_row: - install_method.ensure_app_usable( - app_name=self.name, - app_role=self.role, - show_app_row=show_app_row, + create_or_upgrade_result = self._upgrade_app( + stage_fqn=stage_fqn, + install_method=install_method, + event_sharing=event_sharing, + current_app_row=show_app_row, + policy=policy, + interactive=interactive, ) - # If all the above checks are in order, proceed to upgrade - try: - console.step( - f"Upgrading existing application object {self.name}." - ) - upgrade_result = get_snowflake_facade().upgrade_application( - name=self.name, - install_method=install_method, - stage_fqn=stage_fqn, - ) - print_messages(console, upgrade_result) - - events_definitions = ( - get_snowflake_facade().get_event_definitions( - self.name, self.role - ) - ) - - 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 UpgradeApplicationRestrictionError as err: - console.warning(err.message) - self.drop_application_before_upgrade( - policy=policy, interactive=interactive - ) - except ProgrammingError as err: - 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}" - ) - - try: - create_result = get_snowflake_facade().create_application( - name=self.name, - package_name=package.name, - install_method=install_method, + # 4. Either no existing application found or we performed a drop before the upgrade, so we proceed to create + if create_or_upgrade_result is None: + create_or_upgrade_result = self._create_app( stage_fqn=stage_fqn, - debug_mode=debug_mode, - new_authorize_event_sharing_value=event_sharing.should_authorize_event_sharing_during_create(), - ) - print_messages(console, create_result) - events_definitions = get_snowflake_facade().get_event_definitions( - self.name, self.role + install_method=install_method, + event_sharing=event_sharing, + package=package, ) - 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 - ) + print_messages(self._workspace_ctx.console, create_or_upgrade_result) - # hooks always executed after a create or upgrade - self.execute_post_deploy_hooks() + events_definitions = get_snowflake_facade().get_event_definitions( + self.name, self.role + ) - except ProgrammingError as err: - generic_sql_error_handler(err) + 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 + ) + + # hooks always executed after a create or upgrade + self.execute_post_deploy_hooks() def execute_post_deploy_hooks(self): execute_post_deploy_hooks( 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 1aa96946da..e4f34f0cb1 100644 --- a/src/snowflake/cli/_plugins/nativeapp/same_account_install_method.py +++ b/src/snowflake/cli/_plugins/nativeapp/same_account_install_method.py @@ -9,6 +9,7 @@ ApplicationCreatedExternallyError, ) from snowflake.cli._plugins.stage.manager import StageManager +from snowflake.cli.api.project.util import to_identifier @dataclass @@ -43,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_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index 3207f7d745..a76c28b011 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -18,7 +18,10 @@ from textwrap import dedent from typing import Any, Dict, List -from snowflake.cli._plugins.nativeapp.constants import SPECIAL_COMMENT +from snowflake.cli._plugins.nativeapp.constants import ( + AUTHORIZE_TELEMETRY_COL, + SPECIAL_COMMENT, +) from snowflake.cli._plugins.nativeapp.same_account_install_method import ( SameAccountInstallMethod, ) @@ -528,34 +531,77 @@ def show_release_directives( return cursor.fetchall() def upgrade_application( - self, name: str, install_method: SameAccountInstallMethod, stage_fqn: str + self, + name: str, + current_app_row: dict, + install_method: SameAccountInstallMethod, + stage_fqn: str, + debug_mode: bool | None, + should_authorize_event_sharing: bool | None, + role: str | None = None, + warehouse: str | None = None, ) -> list[tuple[str]]: """ - Upgrades an application object using the provided clause + Upgrades an application object using the provided clauses """ - try: - using_clause = install_method.using_clause(stage_fqn) - upgrade_cursor = self._sql_executor.execute_query( - f"alter application {name} upgrade {using_clause}", - ) - except ProgrammingError as err: - if err.errno in UPGRADE_RESTRICTION_CODES: - raise UpgradeApplicationRestrictionError(err.msg) from err - elif err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY: - get_cli_context().metrics.set_counter( - CLICounterField.EVENT_SHARING_ERROR, 1 + install_method.ensure_app_usable( + app_name=name, + app_role=role, + show_app_row=current_app_row, + ) + # 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}" + ) + + # 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 in UPGRADE_RESTRICTION_CODES: + raise UpgradeApplicationRestrictionError(err.msg) from err + elif 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 + 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." + f"Failed to upgrade application {name} with the following error message:\n" + f"{err.msg}" ) from err - - raise UserInputError( - f"Failed to upgrade application {name} with the following error message:\n" - f"{err.msg}" - ) from err - except Exception as err: - handle_unclassified_error(err, f"Failed to upgrade application {name}.") - return upgrade_cursor.fetchall() + except Exception as err: + handle_unclassified_error(err, f"Failed to upgrade application {name}.") + return upgrade_cursor.fetchall() def create_application( self, @@ -564,7 +610,9 @@ def create_application( install_method: SameAccountInstallMethod, stage_fqn: str, debug_mode: bool | None, - new_authorize_event_sharing_value: bool | None, + should_authorize_event_sharing: bool | None, + role: str | None = None, + warehouse: str | None = None, ) -> list[tuple[str]]: """ Creates a new application object using an application package, @@ -578,41 +626,41 @@ def create_application( debug_mode_clause = f"debug_mode = {initial_debug_mode}" authorize_telemetry_clause = "" - if new_authorize_event_sharing_value is not None: + if should_authorize_event_sharing is not None: self._log.info( "Setting AUTHORIZE_TELEMETRY_EVENT_SHARING to %s", - new_authorize_event_sharing_value, + should_authorize_event_sharing, ) - authorize_telemetry_clause = f" AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(new_authorize_event_sharing_value).upper()}" + authorize_telemetry_clause = f" AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}" using_clause = install_method.using_clause(stage_fqn) - - 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 + 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 + 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." + f"Failed to create application {name} with the following error message:\n" + f"{err.msg}" ) from err - - raise UserInputError( - f"Failed to create application {name} with the following error message:\n" - f"{err.msg}" - ) from err - except Exception as err: - handle_unclassified_error(err, f"Failed to create application {name}.") - return create_cursor.fetchall() + 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 diff --git a/tests/nativeapp/test_run_processor.py b/tests/nativeapp/test_run_processor.py index 1f18422481..c2a86dcfba 100644 --- a/tests/nativeapp/test_run_processor.py +++ b/tests/nativeapp/test_run_processor.py @@ -46,7 +46,10 @@ 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.nativeapp.sf_facade_exceptions import ( + 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 @@ -58,6 +61,7 @@ APPLICATION_NO_LONGER_AVAILABLE, APPLICATION_OWNS_EXTERNAL_OBJECTS, CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, ) @@ -123,6 +127,13 @@ def _get_wm(): 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( @@ -339,13 +350,17 @@ def test_create_dev_app_create_new_w_no_additional_privileges( install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] # 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_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( @@ -371,50 +386,29 @@ def test_create_or_upgrade_dev_app_with_warning( mock_param, mock_conn, mock_execute, + 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_results = [(msg,) for msg in status_messages] + + mock_get_existing_app_info.return_value = existing_app_info + side_effects, expected = mock_execute_helper( [ ( - 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_cursor([("old_role",)], []), mock.call("select current_role()"), ), + (None, mock.call("use role app_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([("old_wh",)], []), + mock.call("select current_warehouse()"), ), + (None, mock.call("use warehouse app_warehouse")), ( mock_cursor([("app_role",)], []), mock.call("select current_role()"), @@ -426,41 +420,14 @@ def test_create_or_upgrade_dev_app_with_warning( 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")), - ] - ) - - 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")) @@ -473,6 +440,34 @@ def test_create_or_upgrade_dev_app_with_warning( 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, + current_app_row=existing_app_info, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] mock_console.warning.assert_has_calls([mock.call(msg) for msg in status_messages]) @@ -566,7 +561,9 @@ def test_create_dev_app_create_new_with_additional_privileges( install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -636,7 +633,9 @@ def test_create_dev_app_create_new_w_missing_warehouse_exception( install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -729,12 +728,14 @@ def test_create_dev_app_incorrect_owner( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "MYAPP", "comment": SPECIAL_COMMENT, "version": LOOSE_FILES_MAGIC_VERSION, "owner": "wrong_owner", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -777,6 +778,11 @@ def test_create_dev_app_incorrect_owner( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -802,12 +808,14 @@ def test_create_dev_app_no_diff_changes( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "MYAPP", "comment": SPECIAL_COMMENT, "version": LOOSE_FILES_MAGIC_VERSION, "owner": "APP_ROLE", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -831,18 +839,6 @@ def test_create_dev_app_no_diff_changes( 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")), ] @@ -866,6 +862,11 @@ def test_create_dev_app_no_diff_changes( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -891,12 +892,14 @@ def test_create_dev_app_w_diff_changes( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "MYAPP", "comment": SPECIAL_COMMENT, "version": LOOSE_FILES_MAGIC_VERSION, "owner": "APP_ROLE", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -920,18 +923,6 @@ def test_create_dev_app_w_diff_changes( 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")), ] @@ -955,6 +946,11 @@ def test_create_dev_app_w_diff_changes( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -1122,7 +1118,9 @@ def test_create_dev_app_create_new_quoted( install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn='"My Package".app_src.stage', debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -1204,7 +1202,9 @@ def test_create_dev_app_create_new_quoted_override( install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn='"My Package".app_src.stage', debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -1236,12 +1236,14 @@ def test_create_dev_app_recreate_app_when_orphaned( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -1294,7 +1296,7 @@ def test_create_dev_app_recreate_app_when_orphaned( 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), + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), cause=ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), ) @@ -1313,6 +1315,11 @@ def test_create_dev_app_recreate_app_when_orphaned( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] assert mock_sql_facade_create_application.mock_calls == [ @@ -1322,7 +1329,9 @@ def test_create_dev_app_recreate_app_when_orphaned( install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -1355,13 +1364,14 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } - # side_effects, expected = mock_execute_helper( + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -1430,7 +1440,7 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( 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), + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), cause=ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), ) mock_sql_facade_create_application.side_effect = mock_cursor( @@ -1448,6 +1458,11 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] assert mock_sql_facade_create_application.mock_calls == [ @@ -1457,7 +1472,9 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -1491,12 +1508,14 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -1559,8 +1578,9 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects - mock_sql_facade_upgrade_application.side_effect = ProgrammingError( - errno=APPLICATION_NO_LONGER_AVAILABLE + 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,)]], [] @@ -1577,6 +1597,11 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] assert mock_sql_facade_create_application.mock_calls == [ @@ -1586,7 +1611,9 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -1670,11 +1697,13 @@ def test_upgrade_app_incorrect_owner( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "APP", "comment": SPECIAL_COMMENT, "owner": "wrong_owner", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -1715,6 +1744,11 @@ def test_upgrade_app_incorrect_owner( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.release_directive(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -1744,11 +1778,13 @@ def test_upgrade_app_succeeds( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -1772,17 +1808,6 @@ def test_upgrade_app_succeeds( 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")), ] @@ -1806,6 +1831,11 @@ def test_upgrade_app_succeeds( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.release_directive(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -1835,11 +1865,13 @@ def test_upgrade_app_fails_generic_error( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -1879,6 +1911,11 @@ def test_upgrade_app_fails_generic_error( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.release_directive(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -1887,6 +1924,7 @@ def test_upgrade_app_fails_generic_error( # 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_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=False @@ -1908,6 +1946,7 @@ def test_upgrade_app_fails_upgrade_restriction_error( mock_conn, mock_typer_confirm, mock_get_existing_app_info, + mock_sql_facade_upgrade_application, mock_execute, policy_param, interactive, @@ -1915,11 +1954,13 @@ def test_upgrade_app_fails_upgrade_restriction_error( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -1932,18 +1973,16 @@ def test_upgrade_app_fails_upgrade_restriction_error( 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 + 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()) @@ -1954,7 +1993,20 @@ def test_upgrade_app_fails_upgrade_restriction_error( install_method=SameAccountInstallMethod.release_directive(), ) assert result.exit_code == expected_code + 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, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] @mock.patch(SQL_FACADE_CREATE_APPLICATION) @@ -1983,12 +2035,14 @@ def test_versioned_app_upgrade_to_unversioned( Ensure that attempting to upgrade from a versioned dev mode application to an unversioned one can succeed given a permissive policy. """ - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", "version": "v1", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -2040,11 +2094,8 @@ def test_versioned_app_upgrade_to_unversioned( 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="Some Error Message.", - errno=93045, - ), + 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,)]], [] @@ -2063,6 +2114,11 @@ def test_versioned_app_upgrade_to_unversioned( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] assert mock_sql_facade_create_application.mock_calls == [ @@ -2072,7 +2128,9 @@ def test_versioned_app_upgrade_to_unversioned( install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -2110,11 +2168,13 @@ def test_upgrade_app_fails_drop_fails( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -2139,10 +2199,10 @@ def test_upgrade_app_fails_drop_fails( ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects - mock_sql_facade_upgrade_application.side_effect = ProgrammingError( - errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + 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): @@ -2157,6 +2217,11 @@ def test_upgrade_app_fails_drop_fails( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.release_directive(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -2190,11 +2255,13 @@ def test_upgrade_app_recreate_app( temp_dir, mock_cursor, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + side_effects, expected = mock_execute_helper( [ ( @@ -2245,8 +2312,9 @@ def test_upgrade_app_recreate_app( ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects - mock_sql_facade_upgrade_application.side_effect = ProgrammingError( - errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + 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,)]], [] @@ -2265,6 +2333,11 @@ def test_upgrade_app_recreate_app( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.release_directive(), stage_fqn=DEFAULT_STAGE_FQN, + current_app_row=mock_get_existing_app_info_result, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] assert mock_sql_facade_create_application.mock_calls == [ @@ -2274,7 +2347,9 @@ def test_upgrade_app_recreate_app( install_method=SameAccountInstallMethod.release_directive(), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] @@ -2416,11 +2491,8 @@ def test_upgrade_app_recreate_app_from_version( ) 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=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - ), + 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,)]], [] @@ -2447,8 +2519,13 @@ def test_upgrade_app_recreate_app_from_version( assert mock_sql_facade_upgrade_application.mock_calls == [ mock.call( name=DEFAULT_APP_ID, + current_app_row=DEFAULT_GET_EXISTING_APP_INFO_RESULT, 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 == [ @@ -2458,7 +2535,9 @@ def test_upgrade_app_recreate_app_from_version( install_method=SameAccountInstallMethod.versioned_dev("v1"), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=True, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, ) ] diff --git a/tests/nativeapp/test_sf_sql_facade.py b/tests/nativeapp/test_sf_sql_facade.py index 46b7e0ab32..27f6aebb7c 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -17,7 +17,11 @@ from unittest.mock import _Call as Call import pytest -from snowflake.cli._plugins.nativeapp.constants import SPECIAL_COMMENT +from snowflake.cli._plugins.nativeapp.constants import ( + AUTHORIZE_TELEMETRY_COL, + COMMENT_COL, + SPECIAL_COMMENT, +) from snowflake.cli._plugins.nativeapp.same_account_install_method import ( SameAccountInstallMethod, ) @@ -37,6 +41,7 @@ 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, @@ -51,6 +56,9 @@ from tests.nativeapp.utils import ( SQL_EXECUTOR_EXECUTE, SQL_EXECUTOR_EXECUTE_QUERIES, + SQL_FACADE__USE_ROLE_OPTIONAL, + SQL_FACADE__USE_WAREHOUSE_OPTIONAL, + SQL_FACADE_GET_APP_PROPERTIES, mock_execute_helper, ) @@ -1646,7 +1654,16 @@ def test_create_stage_raises_insufficient_privileges_error( mock_execute_query.assert_has_calls(expected) -def test_upgrade_application_unversioned(mock_execute_query, mock_cursor): +@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) +@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) +@mock.patch(SQL_FACADE_GET_APP_PROPERTIES) +def test_upgrade_application_unversioned( + mock_get_app_properties, + mock__use_warehouse_optional, + mock__use_role_optional, + mock_execute_query, + mock_cursor, +): app_name = "test_app" stage_fqn = "app_pkg.app_src.stage" @@ -1662,47 +1679,95 @@ def test_upgrade_application_unversioned(mock_execute_query, mock_cursor): sql_facade.upgrade_application( name=app_name, + current_app_row={COMMENT_COL: SPECIAL_COMMENT}, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=stage_fqn, + debug_mode=None, + should_authorize_event_sharing=None, ) mock_execute_query.assert_has_calls(expected) - - -def test_upgrade_application_version_and_patch(mock_execute_query, mock_cursor): + mock_get_app_properties.assert_not_called() + mock__use_role_optional.assert_called_once_with(None) + mock__use_warehouse_optional.assert_called_once_with(None) + + +@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) +@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) +@mock.patch(SQL_FACADE_GET_APP_PROPERTIES) +def test_upgrade_application_version_and_patch( + mock_get_app_properties, + mock__use_warehouse_optional, + mock__use_role_optional, + mock_execute_query, + mock_cursor, +): app_name = "test_app" stage_fqn = "app_pkg.app_src.stage" + role = "mock_role" + wh = "mock_wh" + mock_get_app_properties.return_value = {AUTHORIZE_TELEMETRY_COL: "false"} side_effects, expected = mock_execute_helper( [ ( mock_cursor([], []), mock.call( - f"alter application {app_name} upgrade using version 3 patch 2" + # 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 sql_facade.upgrade_application( name=app_name, + current_app_row={COMMENT_COL: SPECIAL_COMMENT}, install_method=SameAccountInstallMethod.versioned_dev("3", 2), stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=wh, ) mock_execute_query.assert_has_calls(expected) - - -def test_upgrade_application_from_release_directive(mock_execute_query, mock_cursor): + mock_get_app_properties.assert_called_once_with(app_name, role) + mock__use_role_optional.assert_called_once_with(role) + mock__use_warehouse_optional.assert_called_once_with(wh) + + +@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) +@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) +@mock.patch(SQL_FACADE_GET_APP_PROPERTIES) +def test_upgrade_application_from_release_directive( + mock_get_app_properties, + mock__use_warehouse_optional, + mock__use_role_optional, + mock_execute_query, + mock_cursor, +): app_name = "test_app" stage_fqn = "app_pkg.app_src.stage" + role = "mock_role" + wh = "mock_wh" + mock_get_app_properties.return_value = {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 ) ] ) @@ -1710,14 +1775,30 @@ def test_upgrade_application_from_release_directive(mock_execute_query, mock_cur sql_facade.upgrade_application( name=app_name, + current_app_row={COMMENT_COL: SPECIAL_COMMENT}, install_method=SameAccountInstallMethod.release_directive(), stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=wh, ) mock_execute_query.assert_has_calls(expected) - - -def test_upgrade_application_converts_programmingerrors(mock_execute_query): + mock_get_app_properties.assert_called_once_with(app_name, role) + mock__use_role_optional.assert_called_once_with(role) + mock__use_warehouse_optional.assert_called_once_with(wh) + + +@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) +@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) +@mock.patch(SQL_FACADE_GET_APP_PROPERTIES) +def test_upgrade_application_converts_programmingerrors( + mock_get_app_properties, + mock__use_warehouse_optional, + mock__use_role_optional, + mock_execute_query, +): app_name = "test_app" stage_fqn = "app_pkg.app_src.stage" programming_error_message = "programming error message" @@ -1726,7 +1807,7 @@ def test_upgrade_application_converts_programmingerrors(mock_execute_query): [ ( ProgrammingError( - errno=APPLICATION_REQUIRES_TELEMETRY_SHARING, + errno=APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, msg=programming_error_message, ), mock.call(f"alter application {app_name} upgrade using @{stage_fqn}"), @@ -1738,19 +1819,78 @@ def test_upgrade_application_converts_programmingerrors(mock_execute_query): with pytest.raises(UserInputError) as err: sql_facade.upgrade_application( name=app_name, + current_app_row={COMMENT_COL: SPECIAL_COMMENT}, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, ) assert err.match( f"Failed to upgrade application {app_name} with the following error message:\n" ) assert err.match(programming_error_message) + assert err.value.__cause__.errno == APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT mock_execute_query.assert_has_calls(expected) + mock_get_app_properties.assert_not_called() + mock__use_role_optional.assert_called_once_with(None) + mock__use_warehouse_optional.assert_called_once_with(None) + + +@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) +@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) +@mock.patch(SQL_FACADE_GET_APP_PROPERTIES) +def test_upgrade_application_special_message_for_event_sharing_error( + mock_get_app_properties, + mock__use_warehouse_optional, + mock__use_role_optional, + mock_execute_query, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=CANNOT_DISABLE_MANDATORY_TELEMETRY, + ), + mock.call(f"alter application {app_name} upgrade using version v1 "), + ) + ] + ) + mock_execute_query.side_effect = side_effects + with pytest.raises(UserInputError) as err: + sql_facade.upgrade_application( + name=app_name, + current_app_row={COMMENT_COL: SPECIAL_COMMENT}, + install_method=SameAccountInstallMethod.versioned_dev("v1"), + stage_fqn=stage_fqn, + debug_mode=False, + should_authorize_event_sharing=False, + ) -def test_create_application_with_minimal_clauses(mock_execute_query, mock_cursor): + 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." + ) + assert err.value.__cause__.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY + + mock_execute_query.assert_has_calls(expected) + mock_get_app_properties.assert_not_called() + mock__use_role_optional.assert_called_once_with(None) + mock__use_warehouse_optional.assert_called_once_with(None) + + +@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) +@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) +def test_create_application_with_minimal_clauses( + mock__use_warehouse_optional, + mock__use_role_optional, + mock_execute_query, + mock_cursor, +): app_name = "test_app" pkg_name = "test_pkg" stage_fqn = "app_pkg.app_src.stage" @@ -1779,16 +1919,27 @@ def test_create_application_with_minimal_clauses(mock_execute_query, mock_cursor install_method=SameAccountInstallMethod.release_directive(), stage_fqn=stage_fqn, debug_mode=None, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, ) mock_execute_query.assert_has_calls(expected) + mock__use_role_optional.assert_called_once_with(None) + mock__use_warehouse_optional.assert_called_once_with(None) -def test_create_application_with_all_clauses(mock_execute_query, mock_cursor): +@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) +@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) +def test_create_application_with_all_clauses( + mock__use_warehouse_optional, + mock__use_role_optional, + mock_execute_query, + mock_cursor, +): app_name = "test_app" pkg_name = "test_pkg" stage_fqn = "app_pkg.app_src.stage" + role = "mock_role" + wh = "mock_wh" side_effects, expected = mock_execute_helper( [ @@ -1814,13 +1965,21 @@ def test_create_application_with_all_clauses(mock_execute_query, mock_cursor): install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=stage_fqn, debug_mode=True, - new_authorize_event_sharing_value=True, + should_authorize_event_sharing=True, + role=role, + warehouse=wh, ) mock_execute_query.assert_has_calls(expected) + mock__use_role_optional.assert_called_once_with(role) + mock__use_warehouse_optional.assert_called_once_with(wh) -def test_create_application_converts_programmingerrors(mock_execute_query): +@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) +@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) +def test_create_application_converts_programmingerrors( + mock__use_warehouse_optional, mock__use_role_optional, mock_execute_query +): app_name = "test_app" pkg_name = "test_pkg" stage_fqn = "app_pkg.app_src.stage" @@ -1854,12 +2013,66 @@ def test_create_application_converts_programmingerrors(mock_execute_query): install_method=SameAccountInstallMethod.release_directive(), stage_fqn=stage_fqn, debug_mode=None, - new_authorize_event_sharing_value=None, + should_authorize_event_sharing=None, ) assert err.match( f"Failed to create application {app_name} with the following error message:\n" ) assert err.match(programming_error_message) + assert err.value.__cause__.errno == APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT + + mock_execute_query.assert_has_calls(expected) + mock__use_role_optional.assert_called_with(None) + mock__use_warehouse_optional.assert_called_with(None) + + +@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) +@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) +def test_create_application_special_message_for_event_sharing_error( + mock__use_warehouse_optional, mock__use_role_optional, mock_execute_query +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "role" + + 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 + + with 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, + ) + + 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." + ) + assert err.value.__cause__.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING mock_execute_query.assert_has_calls(expected) + mock__use_role_optional.assert_called_with(role) + mock__use_warehouse_optional.assert_called_with(None) diff --git a/tests/nativeapp/utils.py b/tests/nativeapp/utils.py index ddcc25f509..ba1cfc8065 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -70,7 +70,10 @@ SQL_FACADE_MODULE = "snowflake.cli._plugins.nativeapp.sf_facade" SQL_FACADE = f"{SQL_FACADE_MODULE}.SnowflakeSQLFacade" +SQL_FACADE__USE_ROLE_OPTIONAL = f"{SQL_FACADE}._use_role_optional" +SQL_FACADE__USE_WAREHOUSE_OPTIONAL = f"{SQL_FACADE}._use_warehouse_optional" SQL_FACADE_GET_ACCOUNT_EVENT_TABLE = f"{SQL_FACADE}.get_account_event_table" +SQL_FACADE_GET_APP_PROPERTIES = f"{SQL_FACADE}.get_app_properties" SQL_FACADE_EXECUTE_USER_SCRIPT = f"{SQL_FACADE}.execute_user_script" SQL_FACADE_STAGE_EXISTS = f"{SQL_FACADE}.stage_exists" SQL_FACADE_CREATE_SCHEMA = f"{SQL_FACADE}.create_schema" From cab6e8b209251a61a74a17e9e0e48d4b6829ede4 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Mon, 25 Nov 2024 17:23:42 -0500 Subject: [PATCH 07/24] update event sharing tests --- tests/nativeapp/test_event_sharing.py | 102 +++++++++++--------------- 1 file changed, 43 insertions(+), 59 deletions(-) diff --git a/tests/nativeapp/test_event_sharing.py b/tests/nativeapp/test_event_sharing.py index a7abb2ee0d..0b022b0670 100644 --- a/tests/nativeapp/test_event_sharing.py +++ b/tests/nativeapp/test_event_sharing.py @@ -205,7 +205,7 @@ def _setup_mocks_for_app( 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( @@ -218,7 +218,7 @@ def _setup_mocks_for_app( 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( @@ -230,7 +230,7 @@ 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, ) @@ -243,7 +243,7 @@ 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 @@ -312,14 +312,10 @@ def _setup_mocks_for_create_app( side_effects, mock_execute_query_expected = mock_execute_helper(calls) mock_execute_query.side_effect = side_effects - mock_sql_facade_create_application.side_effect = ( - mock_side_effect_error_with_cause( - err=UserInputError(DEFAULT_USER_INPUT_ERROR_MESSAGE), - cause=ProgrammingError(errno=programming_errno), - ) - if programming_errno - else mock_cursor([[(DEFAULT_SUCCESS_MESSAGE,)]], []) + 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, @@ -329,7 +325,9 @@ def _setup_mocks_for_create_app( else SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=None, - new_authorize_event_sharing_value=expected_authorize_telemetry_flag, + should_authorize_event_sharing=expected_authorize_telemetry_flag, + role="app_role", + warehouse="app_warehouse", ) ] @@ -346,11 +344,12 @@ def _setup_mocks_for_upgrade_app( 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", } + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result calls = [ ( @@ -376,41 +375,8 @@ 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, - ), - ), ] - if expected_authorize_telemetry_flag is not None: - calls.append( - ( - ( - ProgrammingError(errno=programming_errno) - if programming_errno - else None - ), - 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( ( @@ -430,16 +396,21 @@ def _setup_mocks_for_upgrade_app( side_effects, mock_execute_query_expected = mock_execute_helper(calls) mock_execute_query.side_effect = side_effects - mock_sql_facade_upgrade_application.side_effect = mock_cursor( + 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, + current_app_row=mock_get_existing_app_info_result, 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] @@ -729,7 +700,7 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no 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, @@ -828,11 +799,11 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ console=mock_console, ) - assert [ + assert expected == [ *mock_execute_query.mock_calls, *mock_sql_facade_upgrade_application.mock_calls, *mock_sql_facade_create_application.mock_calls, - ] == expected + ] mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) @@ -883,9 +854,7 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f 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, @@ -1127,7 +1096,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( @@ -1207,7 +1181,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( @@ -1216,7 +1195,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, @@ -1227,7 +1206,7 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio e.value.message == "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." ) - mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) + mock_console.warning.assert_not_called() @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) @@ -1359,7 +1338,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( From 945544ff46097d1afa2614e3336988b73a51968b Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Mon, 25 Nov 2024 17:31:50 -0500 Subject: [PATCH 08/24] remove unused mock arg --- tests/nativeapp/test_event_sharing.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/nativeapp/test_event_sharing.py b/tests/nativeapp/test_event_sharing.py index 0b022b0670..45263d5ae9 100644 --- a/tests/nativeapp/test_event_sharing.py +++ b/tests/nativeapp/test_event_sharing.py @@ -203,7 +203,6 @@ def _setup_mocks_for_app( expected_shared_events=None, is_prod=False, is_upgrade=False, - existing_app_flag=False, events_definitions_in_app=None, error_raised=None, ): @@ -216,7 +215,6 @@ def _setup_mocks_for_app( 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, error_raised=error_raised, ) @@ -343,7 +341,6 @@ def _setup_mocks_for_upgrade_app( expected_shared_events=None, events_definitions_in_app=None, is_prod=False, - existing_app_flag=False, error_raised=None, ): mock_get_existing_app_info_result = { @@ -627,7 +624,6 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec 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() @@ -702,7 +698,6 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no is_prod=not install_method.is_dev_mode, 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() @@ -775,7 +770,6 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ 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=[ { @@ -856,7 +850,6 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f is_prod=not install_method.is_dev_mode, 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=[ { @@ -942,7 +935,6 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide 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() @@ -1086,7 +1078,6 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused 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=[ { @@ -1171,7 +1162,6 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio 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=[ { From ef653abc0626480540afc5a8b75124b280270e9b Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Tue, 26 Nov 2024 11:18:44 -0500 Subject: [PATCH 09/24] extract app entity properties, move upgrade restriction codes to sql facade exceptions --- .../nativeapp/entities/application.py | 81 ++++++++++--------- .../nativeapp/sf_facade_exceptions.py | 18 +++++ .../cli/_plugins/nativeapp/sf_sql_facade.py | 15 +--- 3 files changed, 61 insertions(+), 53 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index fadb123bcb..f2574abdf5 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -317,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, @@ -413,14 +425,12 @@ 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() 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 @@ -447,7 +457,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() @@ -486,22 +496,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, @@ -515,11 +525,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 @@ -527,7 +537,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, @@ -600,10 +610,7 @@ def _upgrade_app( policy: PolicyBase, interactive: bool, ) -> list[tuple[str]] | None: - console = self._workspace_ctx.console - self._workspace_ctx.console.step( - f"Upgrading existing application object {self.name}." - ) + self.console.step(f"Upgrading existing application object {self.name}.") try: return get_snowflake_facade().upgrade_application( @@ -611,13 +618,13 @@ def _upgrade_app( current_app_row=current_app_row, install_method=install_method, stage_fqn=stage_fqn, - debug_mode=self._entity_model.debug, + debug_mode=self.debug, should_authorize_event_sharing=event_sharing.should_authorize_event_sharing(), role=self.role, warehouse=self.warehouse, ) except UpgradeApplicationRestrictionError as err: - console.warning(err.message) + self.console.warning(err.message) self.drop_application_before_upgrade(policy=policy, interactive=interactive) return None @@ -628,9 +635,7 @@ def _create_app( event_sharing: EventSharingHandler, package: ApplicationPackageEntity, ) -> list[tuple[str]]: - self._workspace_ctx.console.step( - f"Creating new application object {self.name} in account." - ) + self.console.step(f"Creating new application object {self.name} in account.") try: sql_executor = get_sql_executor() @@ -654,7 +659,7 @@ def _create_app( package_name=package.name, install_method=install_method, stage_fqn=stage_fqn, - debug_mode=self._entity_model.debug, + debug_mode=self.debug, should_authorize_event_sharing=event_sharing.should_authorize_event_sharing(), role=self.role, warehouse=self.warehouse, @@ -673,10 +678,10 @@ def create_or_upgrade_app( with sql_executor.use_role(self.role): event_sharing = EventSharingHandler( - telemetry_definition=self._entity_model.telemetry, + telemetry_definition=self.telemetry, deploy_root=package.deploy_root, install_method=install_method, - console=self._workspace_ctx.console, + console=self.console, ) # 1. Need to use a warehouse to create an application object @@ -708,7 +713,7 @@ def create_or_upgrade_app( package=package, ) - print_messages(self._workspace_ctx.console, create_or_upgrade_result) + print_messages(self.console, create_or_upgrade_result) events_definitions = get_snowflake_facade().get_event_definitions( self.name, self.role @@ -725,7 +730,7 @@ def create_or_upgrade_app( 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", @@ -766,21 +771,19 @@ def drop_application_before_upgrade( 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 ) - console.message( + 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) - console.warning( + self.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?" @@ -789,16 +792,16 @@ def drop_application_before_upgrade( if not policy.should_proceed(user_prompt): if interactive: - console.message("Not upgrading the application object.") + self.console.message("Not upgrading the application object.") raise typer.Exit(0) else: - console.message( + self.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}.") + self.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}") diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py index f899308992..146496ee43 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py @@ -11,12 +11,30 @@ # 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_NO_LONGER_AVAILABLE, + CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, + NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, +) 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, +} + def handle_unclassified_error(err: Error | Exception, context: str) -> NoReturn: """ diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index a76c28b011..e74718025e 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -27,6 +27,7 @@ ) from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import ( + UPGRADE_RESTRICTION_CODES, CouldNotUseObjectError, InsufficientPrivilegesError, UnexpectedResultError, @@ -37,16 +38,11 @@ ) from snowflake.cli.api.cli_global_context import get_cli_context from snowflake.cli.api.errno import ( - APPLICATION_NO_LONGER_AVAILABLE, 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_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, - NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, - ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, ) from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.metrics import CLICounterField @@ -60,15 +56,6 @@ from snowflake.cli.api.sql_execution import BaseSqlExecutor, SqlExecutor from snowflake.connector import DictCursor, 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, -} - class SnowflakeSQLFacade: def __init__(self, sql_executor: SqlExecutor | None = None): From da876f055602154ede347d0ef10688c61f84b3ee Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Tue, 26 Nov 2024 13:27:55 -0500 Subject: [PATCH 10/24] move grants for application to sf facade --- .../nativeapp/entities/application.py | 32 ++----- .../cli/_plugins/nativeapp/sf_sql_facade.py | 90 +++++++++++++++++-- src/snowflake/cli/api/constants.py | 3 + 3 files changed, 96 insertions(+), 29 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index f2574abdf5..7cded9afb9 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -84,7 +84,6 @@ from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField from snowflake.cli.api.project.util import ( append_test_resource_suffix, - extract_schema, identifier_for_url, to_identifier, unquote_identifier, @@ -637,26 +636,10 @@ def _create_app( ) -> list[tuple[str]]: self.console.step(f"Creating new application object {self.name} in account.") - try: - sql_executor = get_sql_executor() - 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}" - ) - stage_schema = extract_schema(stage_fqn) - 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}" - ) - except ProgrammingError as err: - generic_sql_error_handler(err) - return get_snowflake_facade().create_application( name=self.name, package_name=package.name, + package_role=package.role, install_method=install_method, stage_fqn=stage_fqn, debug_mode=self.debug, @@ -676,13 +659,14 @@ def create_or_upgrade_app( ): sql_executor = get_sql_executor() + event_sharing = EventSharingHandler( + telemetry_definition=self.telemetry, + deploy_root=package.deploy_root, + install_method=install_method, + console=self.console, + ) + with sql_executor.use_role(self.role): - event_sharing = EventSharingHandler( - telemetry_definition=self.telemetry, - deploy_root=package.deploy_root, - install_method=install_method, - console=self.console, - ) # 1. Need to use a warehouse to create an application object with sql_executor.use_warehouse(self.warehouse): diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index e74718025e..b66bef10f1 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -16,7 +16,7 @@ import logging from contextlib import contextmanager from textwrap import dedent -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from snowflake.cli._plugins.nativeapp.constants import ( AUTHORIZE_TELEMETRY_COL, @@ -37,6 +37,7 @@ 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, @@ -47,6 +48,7 @@ from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.metrics import CLICounterField from snowflake.cli.api.project.util import ( + extract_schema, identifier_to_show_like_pattern, is_valid_unquoted_identifier, to_identifier, @@ -150,6 +152,74 @@ 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, + to_role: str, + role: Optional[str], + ) -> None: + """ + Grants one or more access privileges on a securable object to a role + + Args: + privileges (list[str]): List of privileges to grant to a role + object_type (ObjectType): Type of snowflake object to grant to a role + object_identifier (str): Valid identifier of the snowflake object to grant to a role + to_role (str): Name of the role to grant privileges to + role (Optional[str]): Name of the role to use to grant privileges + """ + comma_separated_privileges_str = ", ".join(privileges) + object_type_and_name = f"{object_type.value.sf_name} {object_identifier}" + + with self._use_role_optional(role): + self._sql_executor.execute_query( + f"grant {comma_separated_privileges_str} on {object_type_and_name} to role {to_role}" + ) + + def _grant_privileges_for_create_application( + self, package_role: str, package_name: str, stage_fqn: str, app_role: str + ): + """ + Grants the required privileges to create an application to an + app role when the package role and the app role are not the same + """ + try: + self._grant_privileges_to_role( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier=package_name, + to_role=app_role, + role=package_role, + ) + + stage_schema = extract_schema(stage_fqn) + self._grant_privileges_to_role( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier=f"{package_name}.{stage_schema}", + to_role=app_role, + role=package_role, + ) + + self._grant_privileges_to_role( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier=stage_fqn, + to_role=app_role, + role=package_role, + ) + except ProgrammingError as err: + raise UserInputError( + f"Failed to grant the required privileges to create an application with the following error message:\n" + f"{err.msg}" + ) from err + except Exception as err: + handle_unclassified_error( + err, "Failed to grant the required privileges to create an application" + ) + def execute_user_script( self, queries: str, @@ -523,10 +593,10 @@ def upgrade_application( current_app_row: dict, install_method: SameAccountInstallMethod, stage_fqn: str, + role: str, + warehouse: str, debug_mode: bool | None, should_authorize_event_sharing: bool | None, - role: str | None = None, - warehouse: str | None = None, ) -> list[tuple[str]]: """ Upgrades an application object using the provided clauses @@ -594,17 +664,27 @@ def create_application( self, name: str, package_name: str, + package_role: str, install_method: SameAccountInstallMethod, stage_fqn: str, + role: str, + warehouse: str, debug_mode: bool | None, should_authorize_event_sharing: bool | None, - role: str | None = None, - warehouse: str | None = None, ) -> list[tuple[str]]: """ Creates a new application object using an application package, running the setup script of the application package """ + + if package_role != role: + self._grant_privileges_for_create_application( + package_role=package_role, + package_name=package_name, + stage_fqn=stage_fqn, + app_role=role, + ) + # by default, applications are created in debug mode when possible; # this can be overridden in the project definition debug_mode_clause = "" diff --git a/src/snowflake/cli/api/constants.py b/src/snowflake/cli/api/constants.py index 2e16e9abdd..2483dd3f1f 100644 --- a/src/snowflake/cli/api/constants.py +++ b/src/snowflake/cli/api/constants.py @@ -62,6 +62,9 @@ class ObjectType(Enum): "image-repository", "image repository", "image repositories" ) GIT_REPOSITORY = ObjectNames("git-repository", "git repository", "git repositories") + APPLICATION_PACKAGE = ObjectNames( + "application-package", "application package", "application packages" + ) def __str__(self): """This makes using this Enum easier in formatted string""" From 34bf5405b95f2ce30e015dd270b43636a0a2b3ff Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Tue, 26 Nov 2024 13:54:28 -0500 Subject: [PATCH 11/24] move grant privileges call to app entity --- .../nativeapp/entities/application.py | 9 +- .../cli/_plugins/nativeapp/sf_sql_facade.py | 93 +++++++++---------- 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index 7cded9afb9..62bd2ef1a5 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -636,10 +636,17 @@ def _create_app( ) -> 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_for_create_application( + package_role=package.role, + package_name=package.name, + stage_fqn=stage_fqn, + app_role=self.role, + ) + return get_snowflake_facade().create_application( name=self.name, package_name=package.name, - package_role=package.role, install_method=install_method, stage_fqn=stage_fqn, debug_mode=self.debug, diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index b66bef10f1..2e025b1ccc 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -178,48 +178,6 @@ def _grant_privileges_to_role( f"grant {comma_separated_privileges_str} on {object_type_and_name} to role {to_role}" ) - def _grant_privileges_for_create_application( - self, package_role: str, package_name: str, stage_fqn: str, app_role: str - ): - """ - Grants the required privileges to create an application to an - app role when the package role and the app role are not the same - """ - try: - self._grant_privileges_to_role( - privileges=["install", "develop"], - object_type=ObjectType.APPLICATION_PACKAGE, - object_identifier=package_name, - to_role=app_role, - role=package_role, - ) - - stage_schema = extract_schema(stage_fqn) - self._grant_privileges_to_role( - privileges=["usage"], - object_type=ObjectType.SCHEMA, - object_identifier=f"{package_name}.{stage_schema}", - to_role=app_role, - role=package_role, - ) - - self._grant_privileges_to_role( - privileges=["read"], - object_type=ObjectType.STAGE, - object_identifier=stage_fqn, - to_role=app_role, - role=package_role, - ) - except ProgrammingError as err: - raise UserInputError( - f"Failed to grant the required privileges to create an application with the following error message:\n" - f"{err.msg}" - ) from err - except Exception as err: - handle_unclassified_error( - err, "Failed to grant the required privileges to create an application" - ) - def execute_user_script( self, queries: str, @@ -660,11 +618,52 @@ def upgrade_application( handle_unclassified_error(err, f"Failed to upgrade application {name}.") return upgrade_cursor.fetchall() + def grant_privileges_for_create_application( + self, package_role: str, package_name: str, stage_fqn: str, app_role: str + ): + """ + Grants the required privileges to create an application to an + app role when the package role and the app role are not the same + """ + try: + self._grant_privileges_to_role( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier=package_name, + to_role=app_role, + role=package_role, + ) + + stage_schema = extract_schema(stage_fqn) + self._grant_privileges_to_role( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier=f"{package_name}.{stage_schema}", + to_role=app_role, + role=package_role, + ) + + self._grant_privileges_to_role( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier=stage_fqn, + to_role=app_role, + role=package_role, + ) + except ProgrammingError as err: + raise UserInputError( + f"Failed to grant the required privileges to create an application with the following error message:\n" + f"{err.msg}" + ) from err + except Exception as err: + handle_unclassified_error( + err, "Failed to grant the required privileges to create an application" + ) + def create_application( self, name: str, package_name: str, - package_role: str, install_method: SameAccountInstallMethod, stage_fqn: str, role: str, @@ -677,14 +676,6 @@ def create_application( running the setup script of the application package """ - if package_role != role: - self._grant_privileges_for_create_application( - package_role=package_role, - package_name=package_name, - stage_fqn=stage_fqn, - app_role=role, - ) - # by default, applications are created in debug mode when possible; # this can be overridden in the project definition debug_mode_clause = "" From cc7aeeee6cf25eb80eb6908afd6eba4e3f06f7c0 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Tue, 26 Nov 2024 14:24:25 -0500 Subject: [PATCH 12/24] separate telemetry error check from rest of upgrade logic and rewrite docstring to match others --- .../cli/_plugins/nativeapp/sf_sql_facade.py | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index 2e025b1ccc..772f6f9f94 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -16,7 +16,7 @@ import logging from contextlib import contextmanager from textwrap import dedent -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from snowflake.cli._plugins.nativeapp.constants import ( AUTHORIZE_TELEMETRY_COL, @@ -157,25 +157,24 @@ def _grant_privileges_to_role( privileges: list[str], object_type: ObjectType, object_identifier: str, - to_role: str, - role: Optional[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 - Args: - privileges (list[str]): List of privileges to grant to a role - object_type (ObjectType): Type of snowflake object to grant to a role - object_identifier (str): Valid identifier of the snowflake object to grant to a role - to_role (str): Name of the role to grant privileges to - role (Optional[str]): Name of the role to use to grant privileges + @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_str = ", ".join(privileges) + comma_separated_privileges = ", ".join(privileges) object_type_and_name = f"{object_type.value.sf_name} {object_identifier}" - with self._use_role_optional(role): + with self._use_role_optional(role_to_use): self._sql_executor.execute_query( - f"grant {comma_separated_privileges_str} on {object_type_and_name} to role {to_role}" + f"grant {comma_separated_privileges} on {object_type_and_name} to role {role_to_grant}" ) def execute_user_script( @@ -579,7 +578,17 @@ def upgrade_application( 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 + raise UserInputError( + f"Failed to upgrade application {name} with the following error message:\n" + f"{err.msg}" + ) from err + 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 = ( @@ -600,9 +609,7 @@ def upgrade_application( f"alter application {name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}" ) except ProgrammingError as err: - if err.errno in UPGRADE_RESTRICTION_CODES: - raise UpgradeApplicationRestrictionError(err.msg) from err - elif err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY: + if err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY: get_cli_context().metrics.set_counter( CLICounterField.EVENT_SHARING_ERROR, 1 ) @@ -611,11 +618,15 @@ def upgrade_application( ) from err raise UserInputError( - f"Failed to upgrade application {name} with the following error message:\n" + f"Failed to set AUTHORIZE_TELEMETRY_EVENT_SHARING when upgrading application {name} with the following error message:\n" f"{err.msg}" ) from err except Exception as err: - handle_unclassified_error(err, f"Failed to upgrade application {name}.") + handle_unclassified_error( + err, + f"Failed to set AUTHORIZE_TELEMETRY_EVENT_SHARING when upgrading application {name}.", + ) + return upgrade_cursor.fetchall() def grant_privileges_for_create_application( @@ -630,8 +641,8 @@ def grant_privileges_for_create_application( privileges=["install", "develop"], object_type=ObjectType.APPLICATION_PACKAGE, object_identifier=package_name, - to_role=app_role, - role=package_role, + role_to_grant=app_role, + role_to_use=package_role, ) stage_schema = extract_schema(stage_fqn) @@ -639,16 +650,16 @@ def grant_privileges_for_create_application( privileges=["usage"], object_type=ObjectType.SCHEMA, object_identifier=f"{package_name}.{stage_schema}", - to_role=app_role, - role=package_role, + role_to_grant=app_role, + role_to_use=package_role, ) self._grant_privileges_to_role( privileges=["read"], object_type=ObjectType.STAGE, object_identifier=stage_fqn, - to_role=app_role, - role=package_role, + role_to_grant=app_role, + role_to_use=package_role, ) except ProgrammingError as err: raise UserInputError( From 8d4e35b8d616c20747cbb3a8872e6cd83eea12eb Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Tue, 26 Nov 2024 16:18:15 -0500 Subject: [PATCH 13/24] move get_existing_app_info to sql facade --- .../nativeapp/entities/application.py | 79 ++++++++----------- .../cli/_plugins/nativeapp/sf_sql_facade.py | 41 ++++++++-- src/snowflake/cli/api/constants.py | 1 + 3 files changed, 68 insertions(+), 53 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index 62bd2ef1a5..5820bb22ee 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -27,7 +27,6 @@ from snowflake.cli._plugins.nativeapp.constants import ( ALLOWED_SPECIAL_COMMENTS, COMMENT_COL, - NAME_COL, OWNER_COL, ) from snowflake.cli._plugins.nativeapp.entities.application_package import ( @@ -605,7 +604,6 @@ def _upgrade_app( stage_fqn: str, install_method: SameAccountInstallMethod, event_sharing: EventSharingHandler, - current_app_row: dict, policy: PolicyBase, interactive: bool, ) -> list[tuple[str]] | None: @@ -614,7 +612,6 @@ def _upgrade_app( try: return get_snowflake_facade().upgrade_application( name=self.name, - current_app_row=current_app_row, install_method=install_method, stage_fqn=stage_fqn, debug_mode=self.debug, @@ -664,8 +661,6 @@ def create_or_upgrade_app( policy: PolicyBase, interactive: bool, ): - sql_executor = get_sql_executor() - event_sharing = EventSharingHandler( telemetry_definition=self.telemetry, deploy_root=package.deploy_root, @@ -673,51 +668,43 @@ def create_or_upgrade_app( console=self.console, ) - with sql_executor.use_role(self.role): - - # 1. Need to use a warehouse to create an application object - with sql_executor.use_warehouse(self.warehouse): - - # 2. Check for an existing application by the same name - show_app_row = self.get_existing_app_info() + # 1. Check for an existing application by the same name + show_app_row = self.get_existing_app_info() - stage_fqn = stage_fqn or package.stage_fqn + stage_fqn = stage_fqn or package.stage_fqn - # 3. If existing application is found, perform a few validations and 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, - current_app_row=show_app_row, - policy=policy, - interactive=interactive, - ) + # 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, + ) - # 4. Either no existing application found or we performed a drop before the upgrade, so 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, - ) + # 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, + ) - print_messages(self.console, create_or_upgrade_result) + print_messages(self.console, create_or_upgrade_result) - events_definitions = get_snowflake_facade().get_event_definitions( - self.name, self.role - ) + 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 - ) + 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) - # hooks always executed after a create or upgrade - self.execute_post_deploy_hooks() + # hooks always executed after a create or upgrade + self.execute_post_deploy_hooks() def execute_post_deploy_hooks(self): execute_post_deploy_hooks( @@ -750,11 +737,7 @@ 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 - ) + return get_snowflake_facade().get_existing_app_info(self.name, self.role) def drop_application_before_upgrade( self, diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index 772f6f9f94..a2f786b01c 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -20,6 +20,7 @@ from snowflake.cli._plugins.nativeapp.constants import ( AUTHORIZE_TELEMETRY_COL, + NAME_COL, SPECIAL_COMMENT, ) from snowflake.cli._plugins.nativeapp.same_account_install_method import ( @@ -54,13 +55,15 @@ to_identifier, to_quoted_identifier, to_string_literal, + unquote_identifier, ) -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() ) @@ -544,10 +547,38 @@ 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: row[NAME_COL] == unquote_identifier(name), + ) + except ProgrammingError as err: + raise UserInputError( + f"Unable to fetch information on application {name} with the following error message:" + f"{err.msg}" + ) + 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, - current_app_row: dict, install_method: SameAccountInstallMethod, stage_fqn: str, role: str, @@ -561,7 +592,7 @@ def upgrade_application( install_method.ensure_app_usable( app_name=name, app_role=role, - show_app_row=current_app_row, + show_app_row=self.get_existing_app_info(name, role), ) # If all the above checks are in order, proceed to upgrade @@ -631,7 +662,7 @@ def upgrade_application( def grant_privileges_for_create_application( self, package_role: str, package_name: str, stage_fqn: str, app_role: str - ): + ) -> None: """ Grants the required privileges to create an application to an app role when the package role and the app role are not the same diff --git a/src/snowflake/cli/api/constants.py b/src/snowflake/cli/api/constants.py index 2483dd3f1f..f587341240 100644 --- a/src/snowflake/cli/api/constants.py +++ b/src/snowflake/cli/api/constants.py @@ -62,6 +62,7 @@ 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" ) From 52af246bed5247d367218a10f5b84bb10dec0b24 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Wed, 27 Nov 2024 16:59:22 -0500 Subject: [PATCH 14/24] change error handling to be default our fault, add list of error codes to check for user fault --- .../nativeapp/entities/application.py | 30 ++++- .../nativeapp/sf_facade_exceptions.py | 74 ++++++++++++ .../cli/_plugins/nativeapp/sf_sql_facade.py | 110 ++++++++---------- src/snowflake/cli/api/errno.py | 48 +++++++- 4 files changed, 192 insertions(+), 70 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index 5820bb22ee..b0bc7c3f1c 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -57,6 +57,7 @@ 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, @@ -83,6 +84,7 @@ from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField from snowflake.cli.api.project.util import ( append_test_resource_suffix, + extract_schema, identifier_for_url, to_identifier, unquote_identifier, @@ -634,11 +636,29 @@ def _create_app( self.console.step(f"Creating new application object {self.name} in account.") if package.role != self.role: - get_snowflake_facade().grant_privileges_for_create_application( - package_role=package.role, - package_name=package.name, - stage_fqn=stage_fqn, - app_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, + ) + + 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, + ) + + 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, ) return get_snowflake_facade().create_application( diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py index 146496ee43..7fdf52797f 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py @@ -18,11 +18,40 @@ 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, + DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, + DOES_NOT_EXIST_OR_NOT_AUTHORIZED, + DUPLICATE_COLUMN_NAME, + INSUFFICIENT_PRIVILEGES, + NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR, + NATIVE_APPLICATION_MANIFEST_INVALID_SYNTAX, + NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY, + NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD, + NO_INDIVIDUAL_PRIVS, + NO_REFERENCE_SET_FOR_DEFINITION, + NO_VERSIONS_AVAILABLE_FOR_ACCOUNT, NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + OBJECT_ALREADY_EXISTS_IN_DOMAIN, + OBJECT_ALREADY_EXISTS_NO_PRIVILEGES, 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 @@ -35,6 +64,51 @@ APPLICATION_NO_LONGER_AVAILABLE, } +CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES = { + APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + # user tried to do something they didn't have permission to + INSUFFICIENT_PRIVILEGES, + NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR, + # user tried to create two objects with the same name + OBJECT_ALREADY_EXISTS_IN_DOMAIN, + APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE, + # user tried to access object that doesn't exist or operation that's not allowed + DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, + # when setup script/manifest/readme isn't on the stage + APPLICATION_FILE_NOT_FOUND_ON_STAGE, + NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD, + # user tried to access an object they don't have permission to + OBJECT_ALREADY_EXISTS_NO_PRIVILEGES, + 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, + # user tried to grant individual privilege on imported objects + NO_INDIVIDUAL_PRIVS, + SNOWSERVICES_IMAGE_REPOSITORY_IMAGE_IMPORT_TO_NATIVE_APP_FAIL, + # user tried to resolve a table/view from a name, but it was not found + DOES_NOT_EXIST_OR_NOT_AUTHORIZED, + 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, + # user tried to create a table with duplicate column names + DUPLICATE_COLUMN_NAME, + 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: """ diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index a2f786b01c..5363b91c35 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -28,6 +28,7 @@ ) 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, @@ -49,7 +50,6 @@ from snowflake.cli.api.identifiers import FQN from snowflake.cli.api.metrics import CLICounterField from snowflake.cli.api.project.util import ( - extract_schema, identifier_to_show_like_pattern, is_valid_unquoted_identifier, to_identifier, @@ -155,7 +155,7 @@ def _use_schema_optional(self, schema_name: str | None): """ return self._use_object_optional(UseObjectType.SCHEMA, schema_name) - def _grant_privileges_to_role( + def grant_privileges_to_role( self, privileges: list[str], object_type: ObjectType, @@ -176,9 +176,15 @@ def _grant_privileges_to_role( object_type_and_name = f"{object_type.value.sf_name} {object_identifier}" with self._use_role_optional(role_to_use): - self._sql_executor.execute_query( - f"grant {comma_separated_privileges} on {object_type_and_name} to role {role_to_grant}" - ) + 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, @@ -588,6 +594,14 @@ def upgrade_application( ) -> 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, @@ -612,10 +626,14 @@ def upgrade_application( except ProgrammingError as err: if err.errno in UPGRADE_RESTRICTION_CODES: raise UpgradeApplicationRestrictionError(err.msg) from err - raise UserInputError( - f"Failed to upgrade application {name} with the following error message:\n" - f"{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}.") @@ -647,11 +665,10 @@ def upgrade_application( 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 - - raise UserInputError( - f"Failed to set AUTHORIZE_TELEMETRY_EVENT_SHARING when upgrading application {name} with the following error message:\n" - f"{err.msg}" - ) 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, @@ -660,48 +677,6 @@ def upgrade_application( return upgrade_cursor.fetchall() - def grant_privileges_for_create_application( - self, package_role: str, package_name: str, stage_fqn: str, app_role: str - ) -> None: - """ - Grants the required privileges to create an application to an - app role when the package role and the app role are not the same - """ - try: - self._grant_privileges_to_role( - privileges=["install", "develop"], - object_type=ObjectType.APPLICATION_PACKAGE, - object_identifier=package_name, - role_to_grant=app_role, - role_to_use=package_role, - ) - - stage_schema = extract_schema(stage_fqn) - self._grant_privileges_to_role( - privileges=["usage"], - object_type=ObjectType.SCHEMA, - object_identifier=f"{package_name}.{stage_schema}", - role_to_grant=app_role, - role_to_use=package_role, - ) - - self._grant_privileges_to_role( - privileges=["read"], - object_type=ObjectType.STAGE, - object_identifier=stage_fqn, - role_to_grant=app_role, - role_to_use=package_role, - ) - except ProgrammingError as err: - raise UserInputError( - f"Failed to grant the required privileges to create an application with the following error message:\n" - f"{err.msg}" - ) from err - except Exception as err: - handle_unclassified_error( - err, "Failed to grant the required privileges to create an application" - ) - def create_application( self, name: str, @@ -716,6 +691,15 @@ def create_application( """ 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; @@ -753,13 +737,17 @@ def create_application( 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 - - raise UserInputError( - f"Failed to create application {name} with the following error message:\n" - f"{err.msg}" - ) 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() diff --git a/src/snowflake/cli/api/errno.py b/src/snowflake/cli/api/errno.py index 36ebaad151..b551f259f6 100644 --- a/src/snowflake/cli/api/errno.py +++ b/src/snowflake/cli/api/errno.py @@ -14,18 +14,58 @@ # 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 -APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT = 93082 + +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 From 6205d723676e19dbc17eb6859a30cc83e5ddba8a Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Thu, 28 Nov 2024 09:56:54 -0500 Subject: [PATCH 15/24] unclassify user errors for get existing app info --- src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index 5363b91c35..b23c203d2a 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -571,11 +571,6 @@ def get_existing_app_info(self, name: str, role: str) -> dict | None: show_obj_cursor, lambda row: row[NAME_COL] == unquote_identifier(name), ) - except ProgrammingError as err: - raise UserInputError( - f"Unable to fetch information on application {name} with the following error message:" - f"{err.msg}" - ) except Exception as err: handle_unclassified_error( err, f"Unable to fetch information on application {name}." From cef433709bd8e948014ece67914c13ebd0a2e5ee Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Thu, 28 Nov 2024 10:09:51 -0500 Subject: [PATCH 16/24] remove error codes to err on the side of caution --- .../nativeapp/sf_facade_exceptions.py | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py index 7fdf52797f..fcb2c88010 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py @@ -32,20 +32,13 @@ CANNOT_GRANT_RESTRICTED_PRIVILEGE_TO_APP_PACKAGE_SHARE, CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, - DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, - DOES_NOT_EXIST_OR_NOT_AUTHORIZED, - DUPLICATE_COLUMN_NAME, - INSUFFICIENT_PRIVILEGES, NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR, NATIVE_APPLICATION_MANIFEST_INVALID_SYNTAX, NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY, NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD, - NO_INDIVIDUAL_PRIVS, NO_REFERENCE_SET_FOR_DEFINITION, NO_VERSIONS_AVAILABLE_FOR_ACCOUNT, NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, - OBJECT_ALREADY_EXISTS_IN_DOMAIN, - OBJECT_ALREADY_EXISTS_NO_PRIVILEGES, ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, ROLE_NOT_ASSIGNED, SNOWSERVICES_IMAGE_MANIFEST_NOT_FOUND, @@ -66,30 +59,18 @@ CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES = { APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, - # user tried to do something they didn't have permission to - INSUFFICIENT_PRIVILEGES, NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR, - # user tried to create two objects with the same name - OBJECT_ALREADY_EXISTS_IN_DOMAIN, APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE, - # user tried to access object that doesn't exist or operation that's not allowed - DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, # when setup script/manifest/readme isn't on the stage APPLICATION_FILE_NOT_FOUND_ON_STAGE, NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD, - # user tried to access an object they don't have permission to - OBJECT_ALREADY_EXISTS_NO_PRIVILEGES, 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, - # user tried to grant individual privilege on imported objects - NO_INDIVIDUAL_PRIVS, SNOWSERVICES_IMAGE_REPOSITORY_IMAGE_IMPORT_TO_NATIVE_APP_FAIL, - # user tried to resolve a table/view from a name, but it was not found - DOES_NOT_EXIST_OR_NOT_AUTHORIZED, APPLICATION_PACKAGE_PATCH_DOES_NOT_EXIST, APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE, SNOWSERVICES_IMAGE_REPOSITORY_FAILS_TO_RETRIEVE_IMAGE_HASH_NEW, @@ -101,8 +82,6 @@ APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND, # user tried installing from release directive and there are none available NO_VERSIONS_AVAILABLE_FOR_ACCOUNT, - # user tried to create a table with duplicate column names - DUPLICATE_COLUMN_NAME, APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE, APPLICATION_INSTANCE_EMPTY_SETUP_SCRIPT, APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS, From 7639f2e640f3574564db8b2f5ee1859279759983 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Thu, 28 Nov 2024 14:42:52 -0500 Subject: [PATCH 17/24] tests for sql facade (1/3) --- .../nativeapp/entities/application.py | 2 +- src/snowflake/cli/api/errno.py | 1 + tests/nativeapp/test_sf_sql_facade.py | 552 +++++++++++++----- tests/nativeapp/utils.py | 9 +- 4 files changed, 412 insertions(+), 152 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index b0bc7c3f1c..697a1a3c3f 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -704,7 +704,7 @@ def create_or_upgrade_app( interactive=interactive, ) - # 3. If no existing application found or we performed a drop before the upgrade, we proceed to create + # 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, diff --git a/src/snowflake/cli/api/errno.py b/src/snowflake/cli/api/errno.py index b551f259f6..c49fd167fa 100644 --- a/src/snowflake/cli/api/errno.py +++ b/src/snowflake/cli/api/errno.py @@ -63,6 +63,7 @@ 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 diff --git a/tests/nativeapp/test_sf_sql_facade.py b/tests/nativeapp/test_sf_sql_facade.py index 27f6aebb7c..f1d3de9bb2 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -20,6 +20,7 @@ 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 ( @@ -38,6 +39,7 @@ 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, @@ -45,6 +47,7 @@ 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 ( @@ -56,9 +59,7 @@ from tests.nativeapp.utils import ( SQL_EXECUTOR_EXECUTE, SQL_EXECUTOR_EXECUTE_QUERIES, - SQL_FACADE__USE_ROLE_OPTIONAL, - SQL_FACADE__USE_WAREHOUSE_OPTIONAL, - SQL_FACADE_GET_APP_PROPERTIES, + assert_programmingerror_cause_with_errno, mock_execute_helper, ) @@ -101,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]], @@ -152,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 @@ -1654,18 +1671,110 @@ def test_create_stage_raises_insufficient_privileges_error( mock_execute_query.assert_has_calls(expected) -@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) -@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) -@mock.patch(SQL_FACADE_GET_APP_PROPERTIES) +@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_app_properties, - mock__use_warehouse_optional, - mock__use_role_optional, + 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( [ @@ -1677,36 +1786,36 @@ def test_upgrade_application_unversioned( ) mock_execute_query.side_effect = side_effects - sql_facade.upgrade_application( - name=app_name, - current_app_row={COMMENT_COL: SPECIAL_COMMENT}, - install_method=SameAccountInstallMethod.unversioned_dev(), - stage_fqn=stage_fqn, - debug_mode=None, - should_authorize_event_sharing=None, - ) + 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] - mock_execute_query.assert_has_calls(expected) - mock_get_app_properties.assert_not_called() - mock__use_role_optional.assert_called_once_with(None) - mock__use_warehouse_optional.assert_called_once_with(None) + 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, + ) -@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) -@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) -@mock.patch(SQL_FACADE_GET_APP_PROPERTIES) def test_upgrade_application_version_and_patch( + mock_get_existing_app_info, + mock_use_role, + mock_use_warehouse, mock_get_app_properties, - mock__use_warehouse_optional, - mock__use_role_optional, mock_execute_query, mock_cursor, ): app_name = "test_app" stage_fqn = "app_pkg.app_src.stage" - role = "mock_role" - wh = "mock_wh" - mock_get_app_properties.return_value = {AUTHORIZE_TELEMETRY_COL: "false"} + role = "test_role" + warehouse = "test_warehouse" side_effects, expected = mock_execute_helper( [ @@ -1728,38 +1837,40 @@ def test_upgrade_application_version_and_patch( ) mock_execute_query.side_effect = side_effects - sql_facade.upgrade_application( - name=app_name, - current_app_row={COMMENT_COL: SPECIAL_COMMENT}, - install_method=SameAccountInstallMethod.versioned_dev("3", 2), - stage_fqn=stage_fqn, - debug_mode=True, - should_authorize_event_sharing=True, - role=role, - warehouse=wh, - ) + 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] - mock_execute_query.assert_has_calls(expected) - mock_get_app_properties.assert_called_once_with(app_name, role) - mock__use_role_optional.assert_called_once_with(role) - mock__use_warehouse_optional.assert_called_once_with(wh) + 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, + ) -@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) -@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) -@mock.patch(SQL_FACADE_GET_APP_PROPERTIES) def test_upgrade_application_from_release_directive( mock_get_app_properties, - mock__use_warehouse_optional, - mock__use_role_optional, + 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 = "mock_role" - wh = "mock_wh" - mock_get_app_properties.return_value = {AUTHORIZE_TELEMETRY_COL: "true"} + 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( [ @@ -1773,34 +1884,34 @@ def test_upgrade_application_from_release_directive( ) mock_execute_query.side_effect = side_effects - sql_facade.upgrade_application( - name=app_name, - current_app_row={COMMENT_COL: SPECIAL_COMMENT}, - install_method=SameAccountInstallMethod.release_directive(), - stage_fqn=stage_fqn, - debug_mode=True, - should_authorize_event_sharing=True, - role=role, - warehouse=wh, - ) + 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] - mock_execute_query.assert_has_calls(expected) - mock_get_app_properties.assert_called_once_with(app_name, role) - mock__use_role_optional.assert_called_once_with(role) - mock__use_warehouse_optional.assert_called_once_with(wh) + 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, + ) -@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) -@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) -@mock.patch(SQL_FACADE_GET_APP_PROPERTIES) -def test_upgrade_application_converts_programmingerrors( - mock_get_app_properties, - mock__use_warehouse_optional, - mock__use_role_optional, +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( @@ -1816,84 +1927,154 @@ def test_upgrade_application_converts_programmingerrors( ) mock_execute_query.side_effect = side_effects - with pytest.raises(UserInputError) as err: + 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, - current_app_row={COMMENT_COL: SPECIAL_COMMENT}, 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) - assert err.value.__cause__.errno == APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT - - mock_execute_query.assert_has_calls(expected) - mock_get_app_properties.assert_not_called() - mock__use_role_optional.assert_called_once_with(None) - mock__use_warehouse_optional.assert_called_once_with(None) -@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) -@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) -@mock.patch(SQL_FACADE_GET_APP_PROPERTIES) def test_upgrade_application_special_message_for_event_sharing_error( + mock_get_existing_app_info, mock_get_app_properties, - mock__use_warehouse_optional, - mock__use_role_optional, + 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} upgrade using version v1 "), - ) + mock.call( + f"alter application {app_name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = FALSE" + ), + ), ] ) mock_execute_query.side_effect = side_effects - with pytest.raises(UserInputError) as err: + 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, - current_app_row={COMMENT_COL: SPECIAL_COMMENT}, 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." ) - assert err.value.__cause__.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY - mock_execute_query.assert_has_calls(expected) - mock_get_app_properties.assert_not_called() - mock__use_role_optional.assert_called_once_with(None) - mock__use_warehouse_optional.assert_called_once_with(None) + +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) -@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) -@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) def test_create_application_with_minimal_clauses( - mock__use_warehouse_optional, - mock__use_role_optional, + 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( [ @@ -1913,33 +2094,36 @@ def test_create_application_with_minimal_clauses( ) mock_execute_query.side_effect = side_effects - 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, - ) + 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] - mock_execute_query.assert_has_calls(expected) - mock__use_role_optional.assert_called_once_with(None) - mock__use_warehouse_optional.assert_called_once_with(None) + 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, + ) -@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) -@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) def test_create_application_with_all_clauses( - mock__use_warehouse_optional, - mock__use_role_optional, + 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 = "mock_role" - wh = "mock_wh" + role = "test_role" + warehouse = "test_warehouse" side_effects, expected = mock_execute_helper( [ @@ -1959,30 +2143,33 @@ def test_create_application_with_all_clauses( ) mock_execute_query.side_effect = side_effects - 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=wh, - ) + 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] - mock_execute_query.assert_has_calls(expected) - mock__use_role_optional.assert_called_once_with(role) - mock__use_warehouse_optional.assert_called_once_with(wh) + 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, + ) -@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) -@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) -def test_create_application_converts_programmingerrors( - mock__use_warehouse_optional, mock__use_role_optional, mock_execute_query +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( @@ -2006,7 +2193,16 @@ def test_create_application_converts_programmingerrors( ) mock_execute_query.side_effect = side_effects - with pytest.raises(UserInputError) as err: + 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, @@ -2014,28 +2210,27 @@ def test_create_application_converts_programmingerrors( 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) - assert err.value.__cause__.errno == APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT - - mock_execute_query.assert_has_calls(expected) - mock__use_role_optional.assert_called_with(None) - mock__use_warehouse_optional.assert_called_with(None) -@mock.patch(SQL_FACADE__USE_ROLE_OPTIONAL) -@mock.patch(SQL_FACADE__USE_WAREHOUSE_OPTIONAL) def test_create_application_special_message_for_event_sharing_error( - mock__use_warehouse_optional, mock__use_role_optional, mock_execute_query + 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 = "role" + role = "test_role" + warehouse = "test_warehouse" side_effects, expected = mock_execute_helper( [ @@ -2057,7 +2252,16 @@ def test_create_application_special_message_for_event_sharing_error( ) mock_execute_query.side_effect = side_effects - with pytest.raises(UserInputError) as err: + 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, @@ -2066,13 +2270,65 @@ def test_create_application_special_message_for_event_sharing_error( 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." ) - assert err.value.__cause__.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING - mock_execute_query.assert_has_calls(expected) - mock__use_role_optional.assert_called_with(role) - mock__use_warehouse_optional.assert_called_with(None) + +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/utils.py b/tests/nativeapp/utils.py index ba1cfc8065..334da16c1f 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -20,6 +20,7 @@ from typing import List, Set import pytest +from snowflake.connector import ProgrammingError from tests.nativeapp.factories import ProjectV10Factory @@ -70,10 +71,7 @@ SQL_FACADE_MODULE = "snowflake.cli._plugins.nativeapp.sf_facade" SQL_FACADE = f"{SQL_FACADE_MODULE}.SnowflakeSQLFacade" -SQL_FACADE__USE_ROLE_OPTIONAL = f"{SQL_FACADE}._use_role_optional" -SQL_FACADE__USE_WAREHOUSE_OPTIONAL = f"{SQL_FACADE}._use_warehouse_optional" SQL_FACADE_GET_ACCOUNT_EVENT_TABLE = f"{SQL_FACADE}.get_account_event_table" -SQL_FACADE_GET_APP_PROPERTIES = f"{SQL_FACADE}.get_app_properties" SQL_FACADE_EXECUTE_USER_SCRIPT = f"{SQL_FACADE}.execute_user_script" SQL_FACADE_STAGE_EXISTS = f"{SQL_FACADE}.stage_exists" SQL_FACADE_CREATE_SCHEMA = f"{SQL_FACADE}.create_schema" @@ -322,3 +320,8 @@ def mock_side_effect_error_with_cause(err: Exception, cause: Exception): 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 From 94e797f541fa5dc428e5609508932f95ecb1e6d1 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Fri, 29 Nov 2024 17:21:49 -0500 Subject: [PATCH 18/24] refactor test_run_processor tests (2/3) --- .../nativeapp/entities/application.py | 81 +- tests/nativeapp/test_run_processor.py | 1140 ++++++----------- tests/nativeapp/utils.py | 3 + 3 files changed, 434 insertions(+), 790 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index 697a1a3c3f..f92952d68d 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -765,50 +765,53 @@ def drop_application_before_upgrade( interactive: bool, cascade: bool = False, ): - 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." ) + 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( - f"The following objects are owned by application {self.name} and need to be dropped:\n{application_objects_str}" + "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) - self.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: - 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 = 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/tests/nativeapp/test_run_processor.py b/tests/nativeapp/test_run_processor.py index c2a86dcfba..3cd8017933 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,7 +47,9 @@ 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, ) @@ -62,12 +65,10 @@ 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, NO_WAREHOUSE_SELECTED_IN_SESSION, ) -from snowflake.cli.api.exceptions import ( - CouldNotUseObjectError, -) from snowflake.cli.api.project.definition_manager import DefinitionManager from snowflake.connector import ProgrammingError from snowflake.connector.cursor import DictCursor @@ -81,6 +82,9 @@ 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, @@ -222,7 +226,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(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -232,34 +238,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()) @@ -272,17 +262,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_FACADE_CREATE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -294,41 +316,13 @@ 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")), - ( - 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,)]], [] ) @@ -342,7 +336,6 @@ 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, @@ -355,13 +348,16 @@ def test_create_dev_app_create_new_w_no_additional_privileges( 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_FACADE_CREATE_APPLICATION) @mock.patch(SQL_FACADE_UPGRADE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -385,7 +381,7 @@ 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, @@ -397,35 +393,7 @@ def test_create_or_upgrade_dev_app_with_warning( 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")), - ( - 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.return_value = status_cursor_results mock_sql_facade_upgrade_application.return_value = status_cursor_results @@ -439,7 +407,7 @@ 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( @@ -461,7 +429,6 @@ def test_create_or_upgrade_dev_app_with_warning( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, - current_app_row=existing_app_info, debug_mode=True, should_authorize_event_sharing=None, role=DEFAULT_ROLE, @@ -469,13 +436,17 @@ def test_create_or_upgrade_dev_app_with_warning( ) ] + 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_FACADE_CREATE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -487,61 +458,14 @@ 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")), - ( - 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,)]], [] ) @@ -553,7 +477,6 @@ 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, @@ -566,12 +489,37 @@ def test_create_dev_app_create_new_with_additional_privileges( 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_FACADE_CREATE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -583,31 +531,12 @@ 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")), - (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), @@ -625,7 +554,6 @@ def test_create_dev_app_create_new_w_missing_warehouse_exception( ) assert err.match("No active warehouse selected in the current session") - assert mock_execute.mock_calls == expected assert mock_sql_facade_create_application.mock_calls == [ mock.call( name=DEFAULT_APP_ID, @@ -642,7 +570,7 @@ def test_create_dev_app_create_new_w_missing_warehouse_exception( # 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( @@ -675,24 +603,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()) @@ -704,13 +615,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_FACADE_UPGRADE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -722,38 +635,19 @@ 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, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "MYAPP", "comment": SPECIAL_COMMENT, "version": LOOSE_FILES_MAGIC_VERSION, "owner": "wrong_owner", } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result - 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_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( err=UserInputError(DEFAULT_USER_INPUT_ERROR_MESSAGE), cause=ProgrammingError( @@ -772,13 +666,11 @@ def test_create_dev_app_incorrect_owner( 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, - current_app_row=mock_get_existing_app_info_result, debug_mode=True, should_authorize_event_sharing=None, role=DEFAULT_ROLE, @@ -790,7 +682,7 @@ def test_create_dev_app_incorrect_owner( # Test create_dev_app with existing application AND diff has no changes @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock.patch(SQL_FACADE_UPGRADE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch( GET_UI_PARAMETERS, return_value={ @@ -802,49 +694,20 @@ 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, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "MYAPP", "comment": SPECIAL_COMMENT, "version": LOOSE_FILES_MAGIC_VERSION, "owner": "APP_ROLE", } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result - 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")), - ( - 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_cursor( [[(DEFAULT_UPGRADE_SUCCESS_MESSAGE,)]], [] ) @@ -856,25 +719,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, - current_app_row=mock_get_existing_app_info_result, 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_FACADE_UPGRADE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -886,49 +750,20 @@ 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, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "MYAPP", "comment": SPECIAL_COMMENT, "version": LOOSE_FILES_MAGIC_VERSION, "owner": "APP_ROLE", } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result - 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")), - ( - 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_cursor( [[(DEFAULT_UPGRADE_SUCCESS_MESSAGE,)]], [] ) @@ -940,25 +775,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, - current_app_row=mock_get_existing_app_info_result, 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_FACADE_UPGRADE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -970,7 +805,6 @@ 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, @@ -982,24 +816,7 @@ 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")), - (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), @@ -1016,14 +833,13 @@ def test_create_dev_app_recreate_w_missing_warehouse_exception( install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected 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_FACADE_CREATE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1035,41 +851,13 @@ 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")), - ( - 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,)]], [] ) @@ -1110,7 +898,6 @@ 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"', @@ -1123,12 +910,15 @@ def test_create_dev_app_create_new_quoted( 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_FACADE_GET_EVENT_DEFINITIONS, return_value=[]) @mock.patch(SQL_FACADE_CREATE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1140,41 +930,13 @@ 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")), - ( - 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,)]], [] ) @@ -1194,19 +956,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 - 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_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 @@ -1216,6 +978,8 @@ def test_create_dev_app_create_new_quoted_override( # 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_connection() @@ -1231,18 +995,19 @@ 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, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result side_effects, expected = mock_execute_helper( [ @@ -1251,44 +1016,7 @@ 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")), (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")), - ( - 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")), ] ) @@ -1315,7 +1043,6 @@ def test_create_dev_app_recreate_app_when_orphaned( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, - current_app_row=mock_get_existing_app_info_result, debug_mode=True, should_authorize_event_sharing=None, role=DEFAULT_ROLE, @@ -1334,6 +1061,33 @@ def test_create_dev_app_recreate_app_when_orphaned( 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 @@ -1344,6 +1098,8 @@ def test_create_dev_app_recreate_app_when_orphaned( # 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_connection() @@ -1359,18 +1115,19 @@ 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, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result side_effects, expected = mock_execute_helper( [ @@ -1379,11 +1136,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_OWNS_EXTERNAL_OBJECTS), mock.call("drop application myapp"), @@ -1392,6 +1144,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( [ @@ -1402,38 +1158,6 @@ 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")), - ( - 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")), ] ) @@ -1458,13 +1182,13 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, - current_app_row=mock_get_existing_app_info_result, 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, @@ -1478,6 +1202,34 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( ) ] + 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 # AND app package has been dropped @@ -1488,6 +1240,8 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( # 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_connection() @@ -1503,18 +1257,19 @@ 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, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result side_effects, expected = mock_execute_helper( [ @@ -1523,11 +1278,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_OWNS_EXTERNAL_OBJECTS), mock.call("drop application myapp"), @@ -1536,43 +1286,15 @@ 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")), ( 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")), ] ) @@ -1597,7 +1319,6 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.unversioned_dev(), stage_fqn=DEFAULT_STAGE_FQN, - current_app_row=mock_get_existing_app_info_result, debug_mode=True, should_authorize_event_sharing=None, role=DEFAULT_ROLE, @@ -1616,10 +1337,40 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje 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, @@ -1632,7 +1383,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( [ @@ -1646,15 +1403,9 @@ def test_upgrade_app_warehouse_error( mock.call("select current_warehouse()"), ), ( - CouldNotUseObjectError( - object_type=ObjectType.WAREHOUSE, name="app_warehouse" - ), + ProgrammingError(errno=DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED), mock.call("use warehouse app_warehouse"), ), - ( - None, - mock.call("use warehouse old_wh"), - ), (None, mock.call("use role old_role")), ] ) @@ -1697,31 +1448,13 @@ def test_upgrade_app_incorrect_owner( temp_dir, mock_cursor, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "APP", "comment": SPECIAL_COMMENT, "owner": "wrong_owner", } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result - 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_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( err=UserInputError("Insufficient privileges to operate on database"), cause=ProgrammingError( @@ -1738,13 +1471,11 @@ def test_upgrade_app_incorrect_owner( install_method=SameAccountInstallMethod.release_directive(), ) err.match("Insufficient privileges to operate on database") - 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, - current_app_row=mock_get_existing_app_info_result, debug_mode=True, should_authorize_event_sharing=None, role=DEFAULT_ROLE, @@ -1755,7 +1486,7 @@ def test_upgrade_app_incorrect_owner( # Test upgrade app method for release directives AND existing app info AND upgrade succeeds @mock.patch(SQL_FACADE_UPGRADE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( @@ -1772,48 +1503,19 @@ 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, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result - 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")), - ( - 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_cursor( [[(DEFAULT_UPGRADE_SUCCESS_MESSAGE,)]], [] ) @@ -1825,24 +1527,22 @@ def test_upgrade_app_succeeds( 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, - current_app_row=mock_get_existing_app_info_result, - debug_mode=True, - should_authorize_event_sharing=None, - role=DEFAULT_ROLE, - warehouse=DEFAULT_WAREHOUSE, - ) - ] + 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_FACADE_UPGRADE_APPLICATION) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( @@ -1859,37 +1559,17 @@ 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, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result - - 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_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( err=UserInputError(DEFAULT_USER_INPUT_ERROR_MESSAGE), cause=ProgrammingError( @@ -1905,13 +1585,11 @@ def test_upgrade_app_fails_generic_error( 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, - current_app_row=mock_get_existing_app_info_result, debug_mode=True, should_authorize_event_sharing=None, role=DEFAULT_ROLE, @@ -1923,9 +1601,9 @@ def test_upgrade_app_fails_generic_error( # 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_EXECUTOR_EXECUTE) @mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=False ) @@ -1945,21 +1623,26 @@ def test_upgrade_app_fails_upgrade_restriction_error( mock_param, mock_conn, mock_typer_confirm, + mock_execute, mock_get_existing_app_info, mock_sql_facade_upgrade_application, - mock_execute, policy_param, interactive, expected_code, temp_dir, mock_cursor, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result + + 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( [ @@ -1968,21 +1651,10 @@ 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")), - (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()) @@ -1994,23 +1666,24 @@ def test_upgrade_app_fails_upgrade_restriction_error( ) assert result.exit_code == expected_code - 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, - current_app_row=mock_get_existing_app_info_result, 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_connection() @@ -2026,6 +1699,8 @@ 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, @@ -2035,13 +1710,12 @@ def test_versioned_app_upgrade_to_unversioned( Ensure that attempting to upgrade from a versioned dev mode application to an unversioned one can succeed given a permissive policy. """ - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", "version": "v1", } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result side_effects, expected = mock_execute_helper( [ @@ -2050,44 +1724,7 @@ 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")), (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")), - ( - 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")), ] ) @@ -2109,31 +1746,54 @@ def test_versioned_app_upgrade_to_unversioned( install_method=SameAccountInstallMethod.unversioned_dev(), ) assert mock_execute.mock_calls == expected - assert mock_sql_facade_upgrade_application.mock_calls == [ + + 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( - name=DEFAULT_APP_ID, - install_method=SameAccountInstallMethod.unversioned_dev(), - stage_fqn=DEFAULT_STAGE_FQN, - current_app_row=mock_get_existing_app_info_result, - debug_mode=True, - should_authorize_event_sharing=None, - role=DEFAULT_ROLE, - warehouse=DEFAULT_WAREHOUSE, - ) - ] - assert mock_sql_facade_create_application.mock_calls == [ + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), 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, - ) + 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 @@ -2168,12 +1828,11 @@ def test_upgrade_app_fails_drop_fails( temp_dir, mock_cursor, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result side_effects, expected = mock_execute_helper( [ @@ -2182,18 +1841,12 @@ 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=1234, ), mock.call("drop application myapp"), ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) @@ -2217,7 +1870,6 @@ def test_upgrade_app_fails_drop_fails( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.release_directive(), stage_fqn=DEFAULT_STAGE_FQN, - current_app_row=mock_get_existing_app_info_result, debug_mode=True, should_authorize_event_sharing=None, role=DEFAULT_ROLE, @@ -2229,6 +1881,8 @@ def test_upgrade_app_fails_drop_fails( # 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( @@ -2249,18 +1903,19 @@ 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, ): - mock_get_existing_app_info_result = { + mock_get_existing_app_info.return_value = { "name": "myapp", "comment": SPECIAL_COMMENT, "owner": "app_role", } - mock_get_existing_app_info.return_value = mock_get_existing_app_info_result side_effects, expected = mock_execute_helper( [ @@ -2269,44 +1924,7 @@ 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")), (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")), - ( - 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")), ] ) @@ -2333,7 +1951,6 @@ def test_upgrade_app_recreate_app( name=DEFAULT_APP_ID, install_method=SameAccountInstallMethod.release_directive(), stage_fqn=DEFAULT_STAGE_FQN, - current_app_row=mock_get_existing_app_info_result, debug_mode=True, should_authorize_event_sharing=None, role=DEFAULT_ROLE, @@ -2352,6 +1969,33 @@ def test_upgrade_app_recreate_app( 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 @@ -2408,6 +2052,8 @@ def test_upgrade_app_from_version_throws_usage_error_two( ) @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( @@ -2428,6 +2074,8 @@ 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, @@ -2448,44 +2096,7 @@ 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")), (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")), - ( - 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")), ] ) @@ -2519,7 +2130,6 @@ def test_upgrade_app_recreate_app_from_version( assert mock_sql_facade_upgrade_application.mock_calls == [ mock.call( name=DEFAULT_APP_ID, - current_app_row=DEFAULT_GET_EXISTING_APP_INFO_RESULT, install_method=SameAccountInstallMethod.versioned_dev("v1"), stage_fqn=DEFAULT_STAGE_FQN, debug_mode=True, @@ -2541,6 +2151,34 @@ def test_upgrade_app_recreate_app_from_version( ) ] + 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 @mock.patch(SQL_EXECUTOR_EXECUTE) diff --git a/tests/nativeapp/utils.py b/tests/nativeapp/utils.py index 334da16c1f..c5a3888237 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -78,6 +78,9 @@ 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( """\ From 1248c2b442f7925280a9f5295973f6ade3218424 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Sat, 30 Nov 2024 17:48:48 -0500 Subject: [PATCH 19/24] refactor event sharing tests --- .../nativeapp/entities/application.py | 4 +- tests/nativeapp/test_event_sharing.py | 124 +++++++++--------- 2 files changed, 64 insertions(+), 64 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index f92952d68d..abd7e14ebb 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -721,7 +721,9 @@ def create_or_upgrade_app( 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) + 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() diff --git a/tests/nativeapp/test_event_sharing.py b/tests/nativeapp/test_event_sharing.py index 45263d5ae9..dea6316d42 100644 --- a/tests/nativeapp/test_event_sharing.py +++ b/tests/nativeapp/test_event_sharing.py @@ -61,6 +61,7 @@ GET_UI_PARAMETERS, SQL_EXECUTOR_EXECUTE, SQL_FACADE_CREATE_APPLICATION, + SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE, SQL_FACADE_UPGRADE_APPLICATION, mock_execute_helper, mock_side_effect_error_with_cause, @@ -251,35 +252,6 @@ def _setup_mocks_for_create_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")), - ( - 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")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), ( mock_cursor( events_definitions_in_app or [], ["name", "type", "sharing", "status"] @@ -289,24 +261,27 @@ 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 @@ -354,15 +329,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")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), ( mock_cursor( events_definitions_in_app or [], ["name", "type", "sharing", "status"] @@ -372,24 +338,27 @@ def _setup_mocks_for_upgrade_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 @@ -399,7 +368,6 @@ def _setup_mocks_for_upgrade_app( mock_sql_facade_upgrade_application_expected = [ mock.call( name=DEFAULT_APP_ID, - current_app_row=mock_get_existing_app_info_result, install_method=SameAccountInstallMethod.release_directive() if is_prod else SameAccountInstallMethod.unversioned_dev(), @@ -416,6 +384,7 @@ def _setup_mocks_for_upgrade_app( @mock.patch(APP_ENTITY_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( @@ -454,6 +423,7 @@ 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, @@ -498,6 +468,7 @@ def test_event_sharing_disabled_no_change_to_current_behavior( @mock.patch(APP_ENTITY_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( @@ -527,6 +498,7 @@ 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, @@ -577,6 +549,7 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit @mock.patch(APP_ENTITY_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( @@ -605,6 +578,7 @@ 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, @@ -651,6 +625,7 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec @mock.patch(APP_ENTITY_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( @@ -679,6 +654,7 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no mock_param, mock_conn, mock_execute_query, + mock_sql_facade_get_event_definitions, mock_sql_facade_upgrade_application, mock_sql_facade_create_application, mock_get_existing_app_info, @@ -725,6 +701,7 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no @mock.patch(APP_ENTITY_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( @@ -751,6 +728,7 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ mock_param, mock_conn, mock_execute_query, + mock_sql_facade_get_event_definitions, mock_sql_facade_upgrade_application, mock_sql_facade_create_application, mock_get_existing_app_info, @@ -805,6 +783,7 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ @mock.patch(APP_ENTITY_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( @@ -831,6 +810,7 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f mock_param, mock_conn, mock_execute_query, + mock_sql_facade_get_event_definitions, mock_sql_facade_upgrade_application, mock_sql_facade_create_application, mock_get_existing_app_info, @@ -890,6 +870,7 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f @mock.patch(APP_ENTITY_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( @@ -916,6 +897,7 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide mock_param, mock_conn, mock_execute_query, + mock_sql_facade_get_event_definitions, mock_sql_facade_upgrade_application, mock_sql_facade_create_application, mock_get_existing_app_info, @@ -962,6 +944,7 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide @mock.patch(APP_ENTITY_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( @@ -988,6 +971,7 @@ 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, @@ -1034,6 +1018,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide @mock.patch(APP_ENTITY_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( @@ -1060,6 +1045,7 @@ 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, @@ -1118,6 +1104,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused @mock.patch(APP_ENTITY_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( @@ -1144,6 +1131,7 @@ 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, @@ -1202,6 +1190,7 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio @mock.patch(APP_ENTITY_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( @@ -1227,6 +1216,7 @@ 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, @@ -1276,6 +1266,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default @mock.patch(APP_ENTITY_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( @@ -1301,6 +1292,7 @@ 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, @@ -1359,6 +1351,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe @mock.patch(APP_ENTITY_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( @@ -1384,6 +1377,7 @@ 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, @@ -1437,6 +1431,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe @mock.patch(APP_ENTITY_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( @@ -1462,6 +1457,7 @@ 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, @@ -1507,6 +1503,7 @@ def test_shared_events_with_no_enabled_mandatory_events_then_error( @mock.patch(APP_ENTITY_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( @@ -1533,6 +1530,7 @@ 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, From d5159d78505e49acb2e5eed0d6cda49c078e5a50 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Sat, 30 Nov 2024 18:01:33 -0500 Subject: [PATCH 20/24] add grant privileges to expected calls --- tests/nativeapp/test_event_sharing.py | 50 ++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/nativeapp/test_event_sharing.py b/tests/nativeapp/test_event_sharing.py index dea6316d42..98f4aac4ad 100644 --- a/tests/nativeapp/test_event_sharing.py +++ b/tests/nativeapp/test_event_sharing.py @@ -40,6 +40,7 @@ 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, @@ -304,7 +305,35 @@ def _setup_mocks_for_create_app( ) ] - return [*mock_execute_query_expected, *mock_sql_facade_create_application_expected] + 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( @@ -460,6 +489,7 @@ def test_event_sharing_disabled_no_change_to_current_behavior( *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) @@ -536,6 +566,7 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit *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 == [ @@ -617,6 +648,7 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec *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) @@ -654,7 +686,7 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no mock_param, mock_conn, mock_execute_query, - mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, mock_sql_facade_upgrade_application, mock_sql_facade_create_application, mock_get_existing_app_info, @@ -693,6 +725,7 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no *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) @@ -728,7 +761,7 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ mock_param, mock_conn, mock_execute_query, - mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, mock_sql_facade_upgrade_application, mock_sql_facade_create_application, mock_get_existing_app_info, @@ -775,6 +808,7 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ *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) @@ -810,7 +844,7 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f mock_param, mock_conn, mock_execute_query, - mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, mock_sql_facade_upgrade_application, mock_sql_facade_create_application, mock_get_existing_app_info, @@ -857,6 +891,7 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f *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 == [ @@ -897,7 +932,7 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide mock_param, mock_conn, mock_execute_query, - mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, mock_sql_facade_upgrade_application, mock_sql_facade_create_application, mock_get_existing_app_info, @@ -936,6 +971,7 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide *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) @@ -1010,6 +1046,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide *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) @@ -1255,6 +1292,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default *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." assert mock_console.warning.mock_calls == [ @@ -1423,6 +1461,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe *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) @@ -1585,6 +1624,7 @@ def test_shared_events_with_authorization_then_success( *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) From 0b4c0fd7543c42b848bdd6b1934aa34208ae0f5a Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Mon, 2 Dec 2024 08:52:59 -0500 Subject: [PATCH 21/24] add support exclusion for application( package) object types --- src/snowflake/cli/api/constants.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/snowflake/cli/api/constants.py b/src/snowflake/cli/api/constants.py index f587341240..d6a3e27f6e 100644 --- a/src/snowflake/cli/api/constants.py +++ b/src/snowflake/cli/api/constants.py @@ -73,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"] From f2d4ff9870459776e7cdad5741af89ff89114753 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Mon, 2 Dec 2024 11:17:40 -0500 Subject: [PATCH 22/24] replace app method of get existing info with facade one --- src/snowflake/cli/_plugins/nativeapp/commands.py | 3 ++- .../_plugins/nativeapp/entities/application.py | 15 ++++++--------- tests/nativeapp/test_manager.py | 5 +++-- 3 files changed, 11 insertions(+), 12 deletions(-) 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 abd7e14ebb..1f794c2db9 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -428,7 +428,9 @@ def action_drop( 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: 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." @@ -689,7 +691,9 @@ def create_or_upgrade_app( ) # 1. Check for an existing application by the same name - show_app_row = self.get_existing_app_info() + show_app_row = get_snowflake_facade().get_existing_app_info( + self.name, self.role + ) stage_fqn = stage_fqn or package.stage_fqn @@ -754,13 +758,6 @@ 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. - """ - return get_snowflake_facade().get_existing_app_info(self.name, self.role) - def drop_application_before_upgrade( self, policy: PolicyBase, 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 From c9f090e5bf5556f11c03bf698195e3a6658eba57 Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Mon, 2 Dec 2024 11:24:44 -0500 Subject: [PATCH 23/24] fix tests after replacing get existing app info with facade version --- tests/nativeapp/test_event_sharing.py | 32 +++++++++---------- tests/nativeapp/test_run_processor.py | 45 +++++++++++++-------------- tests/nativeapp/test_teardown.py | 17 +++++----- tests/nativeapp/utils.py | 1 - 4 files changed, 46 insertions(+), 49 deletions(-) diff --git a/tests/nativeapp/test_event_sharing.py b/tests/nativeapp/test_event_sharing.py index 98f4aac4ad..463e58719e 100644 --- a/tests/nativeapp/test_event_sharing.py +++ b/tests/nativeapp/test_event_sharing.py @@ -58,10 +58,10 @@ 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, @@ -410,7 +410,7 @@ def _setup_mocks_for_upgrade_app( 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) @@ -495,7 +495,7 @@ def test_event_sharing_disabled_no_change_to_current_behavior( 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) @@ -577,7 +577,7 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit ] -@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) @@ -654,7 +654,7 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec 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) @@ -731,7 +731,7 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no 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) @@ -814,7 +814,7 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ 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) @@ -902,7 +902,7 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f ] -@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) @@ -977,7 +977,7 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide 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) @@ -1052,7 +1052,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide 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) @@ -1138,7 +1138,7 @@ 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) @@ -1224,7 +1224,7 @@ 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) @@ -1301,7 +1301,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default ] -@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) @@ -1386,7 +1386,7 @@ 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) @@ -1467,7 +1467,7 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe 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) @@ -1539,7 +1539,7 @@ 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) diff --git a/tests/nativeapp/test_run_processor.py b/tests/nativeapp/test_run_processor.py index 3cd8017933..dc6df30b4b 100644 --- a/tests/nativeapp/test_run_processor.py +++ b/tests/nativeapp/test_run_processor.py @@ -77,7 +77,6 @@ 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, @@ -227,7 +226,7 @@ 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_FACADE_CREATE_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_GRANT_PRIVILEGES_TO_ROLE) @mock_connection() @mock.patch( @@ -302,7 +301,7 @@ def test_create_dev_app_w_warehouse_access_exception( # 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_FACADE_GET_EXISTING_APP_INFO, return_value=None) @mock.patch(SQL_FACADE_CREATE_APPLICATION) @mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @@ -354,7 +353,7 @@ def test_create_dev_app_create_new_w_no_additional_privileges( # 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_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) @@ -443,7 +442,7 @@ def test_create_or_upgrade_dev_app_with_warning( # 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_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) @@ -518,7 +517,7 @@ def test_create_dev_app_create_new_with_additional_privileges( # 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_FACADE_GET_EXISTING_APP_INFO, return_value=None) @mock.patch(SQL_FACADE_CREATE_APPLICATION) @mock_connection() @mock.patch( @@ -622,7 +621,7 @@ def test_create_dev_app_incorrect_properties( # Test create_dev_app with existing application AND incorrect owner -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock_connection() @mock.patch( @@ -680,7 +679,7 @@ def test_create_dev_app_incorrect_owner( # Test create_dev_app with existing application AND diff has no changes -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch( @@ -736,7 +735,7 @@ def test_create_dev_app_no_diff_changes( # Test create_dev_app with existing application AND diff has changes -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @@ -792,7 +791,7 @@ def test_create_dev_app_w_diff_changes( # Test create_dev_app with existing application AND alter throws an error -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock_connection() @mock.patch( @@ -837,7 +836,7 @@ def test_create_dev_app_recreate_w_missing_warehouse_exception( # 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_FACADE_GET_EXISTING_APP_INFO, return_value=None) @mock.patch(SQL_FACADE_CREATE_APPLICATION) @mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @@ -916,7 +915,7 @@ def test_create_dev_app_create_new_quoted( # 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_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() @@ -981,7 +980,7 @@ def test_create_dev_app_create_new_quoted_override( @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, @@ -1101,7 +1100,7 @@ def test_create_dev_app_recreate_app_when_orphaned( @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, @@ -1243,7 +1242,7 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( @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, @@ -1426,7 +1425,7 @@ 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, @@ -1487,7 +1486,7 @@ def test_upgrade_app_incorrect_owner( # Test upgrade app method for release directives AND existing app info AND upgrade succeeds @mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1543,7 +1542,7 @@ def test_upgrade_app_succeeds( # Test upgrade app method for release directives AND existing app info AND upgrade fails due to generic error @mock.patch(SQL_FACADE_UPGRADE_APPLICATION) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, @@ -1602,7 +1601,7 @@ def test_upgrade_app_fails_generic_error( # 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(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=False @@ -1685,7 +1684,7 @@ def test_upgrade_app_fails_upgrade_restriction_error( @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, @@ -1800,7 +1799,7 @@ def test_versioned_app_upgrade_to_unversioned( # 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 ) @@ -1884,7 +1883,7 @@ def test_upgrade_app_fails_drop_fails( @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 ) @@ -2055,7 +2054,7 @@ def test_upgrade_app_from_version_throws_usage_error_two( @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 ) 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 c5a3888237..576fce4436 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -39,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" From 0af48691e87bf59f37022ac0b9e2a4578e892eff Mon Sep 17 00:00:00 2001 From: Marcus Chok Date: Mon, 2 Dec 2024 16:43:39 -0500 Subject: [PATCH 24/24] use _same_identifier for comparison --- src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index b23c203d2a..569898ed2d 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -55,7 +55,6 @@ to_identifier, to_quoted_identifier, to_string_literal, - unquote_identifier, ) from snowflake.cli.api.sql_execution import BaseSqlExecutor from snowflake.cli.api.utils.cursor import find_first_row @@ -568,8 +567,7 @@ def get_existing_app_info(self, name: str, role: str) -> dict | None: ) show_obj_row = find_first_row( - show_obj_cursor, - lambda row: row[NAME_COL] == unquote_identifier(name), + show_obj_cursor, lambda row: _same_identifier(row[NAME_COL], name) ) except Exception as err: handle_unclassified_error(