diff --git a/README.md b/README.md index d2af3ccc28..80225cff5c 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,6 @@ Snowflake CLI is an open-source command-line tool explicitly designed for develo With Snowflake CLI, developers can create, manage, update, and view apps running on Snowflake across workloads such as Streamlit in Snowflake, the Snowflake Native App Framework, Snowpark Container Services, and Snowpark. It supports a range of Snowflake features, including user-defined functions, stored procedures, Streamlit in Snowflake, and SQL execution. -**Note**: Snowflake CLI is in Public Preview (PuPr). - Docs: . Quick start: diff --git a/pyproject.toml b/pyproject.toml index 455babff71..8df59c740e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,9 +33,9 @@ dependencies = [ "requests==2.32.3", "requirements-parser==0.11.0", "setuptools==75.6.0", - 'snowflake.core==0.12.1; python_version < "3.12"', + 'snowflake.core==1.0.2; python_version < "3.12"', "snowflake-connector-python[secure-local-storage]==3.12.3", - 'snowflake-snowpark-python>=1.15.0;python_version < "3.12"', + 'snowflake-snowpark-python>=1.15.0,<1.26.0;python_version < "3.12"', "tomlkit==0.13.2", "typer==0.12.5", "urllib3>=1.24.3,<2.3", @@ -59,11 +59,11 @@ classifiers = [ development = [ "coverage==7.6.8", "pre-commit>=3.5.0", - "pytest==8.3.3", + "pytest==8.3.4", "pytest-randomly==3.16.0", "syrupy==4.8.0", "factory-boy==3.3.1", - "Faker==33.0.0", + "Faker==33.1.0", ] packaging = ["pyinstaller~=6.10"] diff --git a/snyk/requirements.txt b/snyk/requirements.txt index 1c0a975737..e5870050d1 100644 --- a/snyk/requirements.txt +++ b/snyk/requirements.txt @@ -6,9 +6,9 @@ rich==13.9.4 requests==2.32.3 requirements-parser==0.11.0 setuptools==75.6.0 -snowflake.core==0.12.1; python_version < "3.12" +snowflake.core==1.0.2; python_version < "3.12" snowflake-connector-python[secure-local-storage]==3.12.3 -snowflake-snowpark-python>=1.15.0;python_version < "3.12" +snowflake-snowpark-python>=1.15.0,<1.26.0;python_version < "3.12" tomlkit==0.13.2 typer==0.12.5 urllib3>=1.24.3,<2.3 @@ -17,8 +17,8 @@ pip pydantic==2.9.2 coverage==7.6.8 pre-commit>=3.5.0 -pytest==8.3.3 +pytest==8.3.4 pytest-randomly==3.16.0 syrupy==4.8.0 factory-boy==3.3.1 -Faker==33.0.0 +Faker==33.1.0 diff --git a/src/snowflake/cli/_plugins/connection/util.py b/src/snowflake/cli/_plugins/connection/util.py index 5a317b50b5..45c6873e0a 100644 --- a/src/snowflake/cli/_plugins/connection/util.py +++ b/src/snowflake/cli/_plugins/connection/util.py @@ -19,7 +19,6 @@ import os from enum import Enum from functools import lru_cache -from textwrap import dedent from typing import Any, Dict, Optional from click.exceptions import ClickException @@ -57,11 +56,12 @@ class UIParameter(Enum): NA_ENFORCE_MANDATORY_FILTERS = ( "ENFORCE_MANDATORY_FILTERS_FOR_SAME_ACCOUNT_INSTALLATION" ) + NA_FEATURE_RELEASE_CHANNELS = "FEATURE_RELEASE_CHANNELS" def get_ui_parameter( conn: SnowflakeConnection, parameter: UIParameter, default: Any -) -> str: +) -> Any: """ Returns the value of a single UI parameter. If the parameter is not found, the default value is returned. @@ -77,21 +77,19 @@ def get_ui_parameters(conn: SnowflakeConnection) -> Dict[UIParameter, Any]: Returns the UI parameters from the SYSTEM$BOOTSTRAP_DATA_REQUEST function """ - parameters_to_fetch = sorted([param.value for param in UIParameter]) + parameters_to_fetch = [param.value for param in UIParameter] - query = dedent( - f""" - select value['value']::string as PARAM_VALUE, value['name']::string as PARAM_NAME from table(flatten( - input => parse_json(SYSTEM$BOOTSTRAP_DATA_REQUEST()), - path => 'clientParamsInfo' - )) where value['name'] in ('{"', '".join(parameters_to_fetch)}'); - """ - ) + # Parsing of the Json and the filtering is happening here in Snowflake CLI + # in order to avoid requiring a warehouse in Snowflake + query = "call system$bootstrap_data_request('CLIENT_PARAMS_INFO')" + *_, cursor = conn.execute_string(query) - *_, cursor = conn.execute_string(query, cursor_class=DictCursor) + json_map = json.loads(cursor.fetchone()[0]) return { - UIParameter(row["PARAM_NAME"]): row["PARAM_VALUE"] for row in cursor.fetchall() + UIParameter(row["name"]): row["value"] + for row in json_map["clientParamsInfo"] + if row["name"] in parameters_to_fetch } @@ -103,12 +101,7 @@ def is_regionless_redirect(conn: SnowflakeConnection) -> bool: assume it's regionless, as this is true for most production deployments. """ try: - return ( - get_ui_parameter( - conn, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, "true" - ).lower() - == "true" - ) + return get_ui_parameter(conn, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, True) except: log.warning( "Cannot determine regionless redirect; assuming True.", exc_info=True diff --git a/src/snowflake/cli/_plugins/nativeapp/codegen/compiler.py b/src/snowflake/cli/_plugins/nativeapp/codegen/compiler.py index 07302e6356..7681f1fab3 100644 --- a/src/snowflake/cli/_plugins/nativeapp/codegen/compiler.py +++ b/src/snowflake/cli/_plugins/nativeapp/codegen/compiler.py @@ -33,7 +33,6 @@ from snowflake.cli._plugins.nativeapp.codegen.templates.templates_processor import ( TemplatesProcessor, ) -from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag from snowflake.cli.api.cli_global_context import get_cli_context from snowflake.cli.api.console import cli_console as cc from snowflake.cli.api.metrics import CLICounterField @@ -41,15 +40,7 @@ ProcessorMapping, ) -SNOWPARK_PROCESSOR = "snowpark" -NA_SETUP_PROCESSOR = "native app setup" -TEMPLATES_PROCESSOR = "templates" - -_REGISTERED_PROCESSORS_BY_NAME = { - SNOWPARK_PROCESSOR: SnowparkAnnotationProcessor, - NA_SETUP_PROCESSOR: NativeAppSetupProcessor, - TEMPLATES_PROCESSOR: TemplatesProcessor, -} +ProcessorClassType = type[ArtifactProcessor] class NativeAppCompiler: @@ -66,10 +57,28 @@ def __init__( bundle_ctx: BundleContext, ): self._assert_absolute_paths(bundle_ctx) + self._processor_classes_by_name: Dict[str, ProcessorClassType] = {} self._bundle_ctx = bundle_ctx # dictionary of all processors created and shared between different artifact objects. self.cached_processors: Dict[str, ArtifactProcessor] = {} + self.register(SnowparkAnnotationProcessor) + self.register(NativeAppSetupProcessor) + self.register(TemplatesProcessor) + + def register(self, processor_cls: ProcessorClassType): + """ + Registers a processor class to enable. + """ + + name = getattr(processor_cls, "NAME", None) + assert name is not None + + if name in self._processor_classes_by_name: + raise ValueError(f"Processor {name} is already registered") + + self._processor_classes_by_name[str(name)] = processor_cls + @staticmethod def _assert_absolute_paths(bundle_ctx: BundleContext): for name in ["Project", "Deploy", "Bundle", "Generated"]: @@ -128,8 +137,8 @@ def _try_create_processor( if current_processor is not None: return current_processor - processor_factory = _REGISTERED_PROCESSORS_BY_NAME.get(processor_name) - if processor_factory is None: + processor_cls = self._processor_classes_by_name.get(processor_name) + if processor_cls is None: # No registered processor with the specified name return None @@ -141,7 +150,7 @@ def _try_create_processor( processor_ctx.generated_root = ( self._bundle_ctx.generated_root / processor_subdirectory ) - current_processor = processor_factory(processor_ctx) + current_processor = processor_cls(processor_ctx) self.cached_processors[processor_name] = current_processor return current_processor @@ -154,6 +163,18 @@ def _should_invoke_processors(self): return False def _is_enabled(self, processor: ProcessorMapping) -> bool: - if processor.name.lower() == NA_SETUP_PROCESSOR: - return FeatureFlag.ENABLE_NATIVE_APP_PYTHON_SETUP.is_enabled() - return True + """ + Determines is a process is enabled. All processors are considered enabled + unless they are explicitly disabled, typically via a feature flag. + """ + processor_name = processor.name.lower() + processor_cls = self._processor_classes_by_name.get(processor_name) + if processor_cls is None: + # Unknown processor, consider it enabled, even though trying to + # invoke it later will raise an exception + return True + + # if the processor class defines a static method named "is_enabled", then + # call it. Otherwise, it's considered enabled by default. + is_enabled_fn = getattr(processor_cls, "is_enabled", lambda: True) + return is_enabled_fn() diff --git a/src/snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py b/src/snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py index 06be6f01e8..b643f66304 100644 --- a/src/snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +++ b/src/snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py @@ -36,6 +36,7 @@ SandboxEnvBuilder, execute_script_in_sandbox, ) +from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag from snowflake.cli._plugins.stage.diff import to_stage_path from snowflake.cli.api.console import cli_console as cc from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import ( @@ -74,9 +75,15 @@ def safe_set(d: dict, *keys: str, **kwargs) -> None: class NativeAppSetupProcessor(ArtifactProcessor): + NAME = "native app setup" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + @staticmethod + def is_enabled() -> bool: + return FeatureFlag.ENABLE_NATIVE_APP_PYTHON_SETUP.is_enabled() + def process( self, artifact_to_process: PathMapping, diff --git a/src/snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py b/src/snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py index 0241899388..58a9eb2baa 100644 --- a/src/snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +++ b/src/snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py @@ -164,6 +164,8 @@ class SnowparkAnnotationProcessor(ArtifactProcessor): and generate SQL code for creation of extension functions based on those discovered objects. """ + NAME = "snowpark" + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py b/src/snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py index 9e38eecc2c..b6984f67c2 100644 --- a/src/snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +++ b/src/snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py @@ -49,6 +49,8 @@ class TemplatesProcessor(ArtifactProcessor): Processor class to perform template expansion on all relevant artifacts (specified in the project definition file). """ + NAME = "templates" + def expand_templates_in_file( self, src: Path, dest: Path, template_context: dict[str, Any] | None = None ) -> None: @@ -58,38 +60,45 @@ def expand_templates_in_file( if src.is_dir(): return - with self.edit_file(dest) as file: - if not has_client_side_templates(file.contents) and not ( - _is_sql_file(dest) and has_sql_templates(file.contents) - ): - return - - src_file_name = src.relative_to(self._bundle_ctx.project_root) - cc.step(f"Expanding templates in {src_file_name}") - with cc.indented(): - try: - jinja_env = ( - choose_sql_jinja_env_based_on_template_syntax( - file.contents, reference_name=src_file_name + src_file_name = src.relative_to(self._bundle_ctx.project_root) + + try: + with self.edit_file(dest) as file: + if not has_client_side_templates(file.contents) and not ( + _is_sql_file(dest) and has_sql_templates(file.contents) + ): + return + cc.step(f"Expanding templates in {src_file_name}") + with cc.indented(): + try: + jinja_env = ( + choose_sql_jinja_env_based_on_template_syntax( + file.contents, reference_name=src_file_name + ) + if _is_sql_file(dest) + else get_client_side_jinja_env() + ) + expanded_template = jinja_env.from_string(file.contents).render( + template_context or get_cli_context().template_context ) - if _is_sql_file(dest) - else get_client_side_jinja_env() - ) - expanded_template = jinja_env.from_string(file.contents).render( - template_context or get_cli_context().template_context - ) - - # For now, we are printing the source file path in the error message - # instead of the destination file path to make it easier for the user - # to identify the file that has the error, and edit the correct file. - except jinja2.TemplateSyntaxError as e: - raise InvalidTemplateInFileError(src_file_name, e, e.lineno) from e - - except jinja2.UndefinedError as e: - raise InvalidTemplateInFileError(src_file_name, e) from e - - if expanded_template != file.contents: - file.edited_contents = expanded_template + + # For now, we are printing the source file path in the error message + # instead of the destination file path to make it easier for the user + # to identify the file that has the error, and edit the correct file. + except jinja2.TemplateSyntaxError as e: + raise InvalidTemplateInFileError( + src_file_name, e, e.lineno + ) from e + + except jinja2.UndefinedError as e: + raise InvalidTemplateInFileError(src_file_name, e) from e + + if expanded_template != file.contents: + file.edited_contents = expanded_template + except UnicodeDecodeError as err: + cc.warning( + f"Could not read file {src_file_name}, error: {err.reason}. Skipping this file." + ) @span("templates_processor") def process( diff --git a/src/snowflake/cli/_plugins/nativeapp/commands.py b/src/snowflake/cli/_plugins/nativeapp/commands.py index b60ea157fa..55ca0019a0 100644 --- a/src/snowflake/cli/_plugins/nativeapp/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/commands.py @@ -31,6 +31,7 @@ from snowflake.cli._plugins.nativeapp.entities.application_package import ( ApplicationPackageEntityModel, ) +from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade from snowflake.cli._plugins.nativeapp.v2_conversions.compat import ( find_entity, force_project_definition_v2, @@ -198,7 +199,7 @@ def app_open( ) app_id = options["app_entity_id"] app = ws.get_entity(app_id) - if app.get_existing_app_info(): + if get_snowflake_facade().get_existing_app_info(app.name, app.role): typer.launch(app.get_snowsight_url()) return MessageResult(f"Snowflake Native App opened in browser.") else: diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application.py b/src/snowflake/cli/_plugins/nativeapp/entities/application.py index 040e60e82f..5057f86c60 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application.py @@ -13,7 +13,6 @@ from pydantic import Field, field_validator from snowflake.cli._plugins.connection.util import ( UIParameter, - get_ui_parameter, make_snowsight_url, ) from snowflake.cli._plugins.nativeapp.artifacts import ( @@ -26,11 +25,8 @@ ) from snowflake.cli._plugins.nativeapp.constants import ( ALLOWED_SPECIAL_COMMENTS, - AUTHORIZE_TELEMETRY_COL, COMMENT_COL, - NAME_COL, OWNER_COL, - SPECIAL_COMMENT, ) from snowflake.cli._plugins.nativeapp.entities.application_package import ( ApplicationPackageEntity, @@ -53,10 +49,14 @@ SameAccountInstallMethod, ) from snowflake.cli._plugins.nativeapp.sf_facade import get_snowflake_facade +from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import ( + UpgradeApplicationRestrictionError, +) from snowflake.cli._plugins.nativeapp.utils import needs_confirmation from snowflake.cli._plugins.workspace.context import ActionContext from snowflake.cli.api.cli_global_context import get_cli_context, span from snowflake.cli.api.console.abc import AbstractConsole +from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.entities.common import ( EntityBase, attach_spans_to_entity_actions, @@ -71,13 +71,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 +92,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}) @@ -138,18 +123,12 @@ def __init__( self._is_dev_mode = install_method.is_dev_mode self._metrics = get_cli_context().metrics self._console = console - connection = get_sql_executor()._conn # noqa: SLF001 - self._event_sharing_enabled = ( - get_ui_parameter( - connection, UIParameter.NA_EVENT_SHARING_V2, "true" - ).lower() - == "true" + + self._event_sharing_enabled = get_snowflake_facade().get_ui_parameter( + UIParameter.NA_EVENT_SHARING_V2, True ) - self._event_sharing_enforced = ( - get_ui_parameter( - connection, UIParameter.NA_ENFORCE_MANDATORY_FILTERS, "true" - ).lower() - == "true" + self._event_sharing_enforced = get_snowflake_facade().get_ui_parameter( + UIParameter.NA_ENFORCE_MANDATORY_FILTERS, True ) self._share_mandatory_events = ( @@ -191,11 +170,11 @@ def __init__( def _contains_mandatory_events(self, events_definitions: List[Dict[str, str]]): return any(event["sharing"] == "MANDATORY" for event in events_definitions) - def should_authorize_event_sharing_during_create( + def should_authorize_event_sharing( self, ) -> Optional[bool]: """ - Determines whether event sharing should be authorized during the creation of the application object. + Determines whether event sharing should be authorized. Outputs: - None: Event sharing should not be updated or explicitly set. @@ -208,35 +187,6 @@ def should_authorize_event_sharing_during_create( return self._share_mandatory_events - def should_authorize_event_sharing_after_upgrade( - self, - upgraded_app_properties: Dict[str, str], - ) -> Optional[bool]: - """ - Determines whether event sharing should be authorized after upgrading the application object. - - :param upgraded_app_properties: The properties of the application after upgrading. - - Outputs: - - None: Event sharing should not be updated or explicitly set. - - True: Event sharing should be authorized. - - False: Event sharing should be disabled. - """ - - if not self._event_sharing_enabled: - return None - - current_app_authorization = ( - upgraded_app_properties.get(AUTHORIZE_TELEMETRY_COL, "false").lower() - == "true" - ) - - # Skip the update if the current value is the same as the one we want to set - if current_app_authorization == self._share_mandatory_events: - return None - - return self._share_mandatory_events - def event_sharing_warning(self, message: str): """ Logs a warning message about event sharing, and emits an event sharing warning metric. @@ -360,6 +310,18 @@ def post_deploy_hooks(self) -> list[PostDeployHook] | None: model = self._entity_model return model.meta and model.meta.post_deploy + @property + def console(self) -> AbstractConsole: + return self._workspace_ctx.console + + @property + def debug(self) -> bool | None: + return self._entity_model.debug + + @property + def telemetry(self) -> EventSharingTelemetry | None: + return self._entity_model.telemetry + def action_deploy( self, action_ctx: ActionContext, @@ -456,14 +418,14 @@ def action_drop( """ Attempts to drop the application object if all validations and user prompts allow so. """ - console = self._workspace_ctx.console - needs_confirm = True # 1. If existing application is not found, exit gracefully - show_obj_row = self.get_existing_app_info() + show_obj_row = get_snowflake_facade().get_existing_app_info( + self.name, self.role + ) if show_obj_row is None: - console.warning( + self.console.warning( f"Role {self.role} does not own any application object with the name {self.name}, or the application object does not exist." ) return @@ -490,7 +452,7 @@ def action_drop( ) ) if not should_drop_object: - console.message(f"Did not drop application object {self.name}.") + self.console.message(f"Did not drop application object {self.name}.") # The user desires to keep the app, therefore we can't proceed since it would # leave behind an orphan app when we get to dropping the package raise typer.Abort() @@ -529,22 +491,22 @@ def action_drop( if has_objects_to_drop: if cascade is True: # If the user explicitly passed the --cascade flag - console.message(cascade_true_message) - with console.indented(): + self.console.message(cascade_true_message) + with self.console.indented(): for obj in application_objects: - console.message(_application_object_to_str(obj)) + self.console.message(_application_object_to_str(obj)) elif cascade is False: # If the user explicitly passed the --no-cascade flag - console.message(cascade_false_message) - with console.indented(): + self.console.message(cascade_false_message) + with self.console.indented(): for obj in application_objects: - console.message(_application_object_to_str(obj)) + self.console.message(_application_object_to_str(obj)) elif interactive: # If the user didn't pass any cascade flag and the session is interactive - console.message(message_prefix) - with console.indented(): + self.console.message(message_prefix) + with self.console.indented(): for obj in application_objects: - console.message(_application_object_to_str(obj)) + self.console.message(_application_object_to_str(obj)) user_response = typer.prompt( interactive_prompt, show_default=False, @@ -558,11 +520,11 @@ def action_drop( raise typer.Abort() else: # Else abort since we don't know what to do and can't ask the user - console.message(message_prefix) - with console.indented(): + self.console.message(message_prefix) + with self.console.indented(): for obj in application_objects: - console.message(_application_object_to_str(obj)) - console.message(non_interactive_abort) + self.console.message(_application_object_to_str(obj)) + self.console.message(non_interactive_abort) raise typer.Abort() elif cascade is None: # If there's nothing to drop, set cascade to an explicit False value @@ -570,7 +532,7 @@ def action_drop( # 4. All validations have passed, drop object drop_generic_object( - console=console, + console=self.console, object_type="application", object_name=self.name, role=self.role, @@ -634,182 +596,138 @@ def get_objects_owned_by_application(self) -> List[ApplicationOwnedObject]: ).fetchall() return [{"name": row[1], "type": row[2]} for row in results] - @span("update_app_object") - def create_or_upgrade_app( + def _upgrade_app( self, - package: ApplicationPackageEntity, stage_fqn: str, install_method: SameAccountInstallMethod, + event_sharing: EventSharingHandler, policy: PolicyBase, interactive: bool, - ): - model = self._entity_model - console = self._workspace_ctx.console - debug_mode = model.debug + ) -> list[tuple[str]] | None: + self.console.step(f"Upgrading existing application object {self.name}.") - stage_fqn = stage_fqn or package.stage_fqn - stage_schema = extract_schema(stage_fqn) - - sql_executor = get_sql_executor() - with sql_executor.use_role(self.role): - event_sharing = EventSharingHandler( - telemetry_definition=model.telemetry, - deploy_root=package.deploy_root, + try: + return get_snowflake_facade().upgrade_application( + name=self.name, install_method=install_method, - console=console, + stage_fqn=stage_fqn, + debug_mode=self.debug, + should_authorize_event_sharing=event_sharing.should_authorize_event_sharing(), + role=self.role, + warehouse=self.warehouse, ) + except UpgradeApplicationRestrictionError as err: + self.console.warning(err.message) + self.drop_application_before_upgrade(policy=policy, interactive=interactive) + return None - # 1. Need to use a warehouse to create an application object - with sql_executor.use_warehouse(self.warehouse): + def _create_app( + self, + stage_fqn: str, + install_method: SameAccountInstallMethod, + event_sharing: EventSharingHandler, + package: ApplicationPackageEntity, + ) -> list[tuple[str]]: + self.console.step(f"Creating new application object {self.name} in account.") + + if package.role != self.role: + get_snowflake_facade().grant_privileges_to_role( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier=package.name, + role_to_grant=self.role, + role_to_use=package.role, + ) - # 2. Check for an existing application by the same name - show_app_row = self.get_existing_app_info() + stage_schema = extract_schema(stage_fqn) + get_snowflake_facade().grant_privileges_to_role( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier=f"{package.name}.{stage_schema}", + role_to_grant=self.role, + role_to_use=package.role, + ) - # 3. If existing application is found, perform a few validations and upgrade the application object. - if show_app_row: - install_method.ensure_app_usable( - app_name=self.name, - app_role=self.role, - show_app_row=show_app_row, - ) + get_snowflake_facade().grant_privileges_to_role( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier=stage_fqn, + role_to_grant=self.role, + role_to_use=package.role, + ) - # If all the above checks are in order, proceed to upgrade - try: - console.step( - f"Upgrading existing application object {self.name}." - ) - using_clause = install_method.using_clause(stage_fqn) - upgrade_cursor = sql_executor.execute_query( - f"alter application {self.name} upgrade {using_clause}", - ) - print_messages(console, upgrade_cursor) + return get_snowflake_facade().create_application( + name=self.name, + package_name=package.name, + install_method=install_method, + stage_fqn=stage_fqn, + debug_mode=self.debug, + should_authorize_event_sharing=event_sharing.should_authorize_event_sharing(), + role=self.role, + warehouse=self.warehouse, + ) - events_definitions = ( - get_snowflake_facade().get_event_definitions( - self.name, self.role - ) - ) + @span("update_app_object") + def create_or_upgrade_app( + self, + package: ApplicationPackageEntity, + stage_fqn: str, + install_method: SameAccountInstallMethod, + policy: PolicyBase, + interactive: bool, + ): + event_sharing = EventSharingHandler( + telemetry_definition=self.telemetry, + deploy_root=package.deploy_root, + install_method=install_method, + console=self.console, + ) - app_properties = get_snowflake_facade().get_app_properties( - self.name, self.role - ) - new_authorize_event_sharing_value = ( - event_sharing.should_authorize_event_sharing_after_upgrade( - app_properties, - ) - ) - if new_authorize_event_sharing_value is not None: - log.info( - "Setting telemetry sharing authorization to %s", - new_authorize_event_sharing_value, - ) - sql_executor.execute_query( - f"alter application {self.name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(new_authorize_event_sharing_value).upper()}" - ) - events_to_share = event_sharing.events_to_share( - events_definitions - ) - if events_to_share is not None: - get_snowflake_facade().share_telemetry_events( - self.name, events_to_share - ) - - if install_method.is_dev_mode: - # if debug_mode is present (controlled), ensure it is up-to-date - if debug_mode is not None: - sql_executor.execute_query( - f"alter application {self.name} set debug_mode = {debug_mode}" - ) - - # hooks always executed after a create or upgrade - self.execute_post_deploy_hooks() - return - - except ProgrammingError as err: - if err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY: - event_sharing.event_sharing_error( - "Could not disable telemetry event sharing for the application because it contains mandatory events. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file.", - err, - ) - elif err.errno in UPGRADE_RESTRICTION_CODES: - console.warning(err.msg) - self.drop_application_before_upgrade( - policy=policy, interactive=interactive - ) - else: - generic_sql_error_handler(err=err) - - # 4. With no (more) existing application objects, create an application object using the release directives - console.step(f"Creating new application object {self.name} in account.") - - if self.role != package.role: - with sql_executor.use_role(package.role): - sql_executor.execute_query( - f"grant install, develop on application package {package.name} to role {self.role}" - ) - sql_executor.execute_query( - f"grant usage on schema {package.name}.{stage_schema} to role {self.role}" - ) - sql_executor.execute_query( - f"grant read on stage {stage_fqn} to role {self.role}" - ) + # 1. Check for an existing application by the same name + show_app_row = get_snowflake_facade().get_existing_app_info( + self.name, self.role + ) - try: - # by default, applications are created in debug mode when possible; - # this can be overridden in the project definition - debug_mode_clause = "" - if install_method.is_dev_mode: - initial_debug_mode = ( - debug_mode if debug_mode is not None else True - ) - debug_mode_clause = f"debug_mode = {initial_debug_mode}" + stage_fqn = stage_fqn or package.stage_fqn - authorize_telemetry_clause = "" - new_authorize_event_sharing_value = ( - event_sharing.should_authorize_event_sharing_during_create() - ) - if new_authorize_event_sharing_value is not None: - log.info( - "Setting AUTHORIZE_TELEMETRY_EVENT_SHARING to %s", - new_authorize_event_sharing_value, - ) - authorize_telemetry_clause = f" AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(new_authorize_event_sharing_value).upper()}" - - using_clause = install_method.using_clause(stage_fqn) - create_cursor = sql_executor.execute_query( - dedent( - f"""\ - create application {self.name} - from application package {package.name} {using_clause} {debug_mode_clause}{authorize_telemetry_clause} - comment = {SPECIAL_COMMENT} - """ - ), - ) - print_messages(console, create_cursor) - events_definitions = get_snowflake_facade().get_event_definitions( - self.name, self.role - ) + # 2. If existing application is found, try to upgrade the application object. + create_or_upgrade_result = None + if show_app_row: + create_or_upgrade_result = self._upgrade_app( + stage_fqn=stage_fqn, + install_method=install_method, + event_sharing=event_sharing, + policy=policy, + interactive=interactive, + ) - events_to_share = event_sharing.events_to_share(events_definitions) - if events_to_share is not None: - get_snowflake_facade().share_telemetry_events( - self.name, events_to_share - ) + # 3. If no existing application found, or we performed a drop before the upgrade, we proceed to create + if create_or_upgrade_result is None: + create_or_upgrade_result = self._create_app( + stage_fqn=stage_fqn, + install_method=install_method, + event_sharing=event_sharing, + package=package, + ) - # hooks always executed after a create or upgrade - self.execute_post_deploy_hooks() + print_messages(self.console, create_or_upgrade_result) - except ProgrammingError as err: - if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING: - event_sharing.event_sharing_error( - "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file.", - err, - ) - generic_sql_error_handler(err) + events_definitions = get_snowflake_facade().get_event_definitions( + self.name, self.role + ) + + events_to_share = event_sharing.events_to_share(events_definitions) + if events_to_share is not None: + get_snowflake_facade().share_telemetry_events( + self.name, events_to_share, self.role + ) + + # hooks always executed after a create or upgrade + self.execute_post_deploy_hooks() def execute_post_deploy_hooks(self): execute_post_deploy_hooks( - console=self._workspace_ctx.console, + console=self.console, project_root=self.project_root, post_deploy_hooks=self.post_deploy_hooks, deployed_object_type="application", @@ -833,69 +751,59 @@ def use_application_warehouse(self): ) ) - def get_existing_app_info(self) -> Optional[dict]: - """ - Check for an existing application object by the same name as in project definition, in account. - It executes a 'show applications like' query and returns the result as single row, if one exists. - """ - sql_executor = get_sql_executor() - with sql_executor.use_role(self.role): - return sql_executor.show_specific_object( - "applications", self.name, name_col=NAME_COL - ) - def drop_application_before_upgrade( self, policy: PolicyBase, interactive: bool, cascade: bool = False, ): - console = self._workspace_ctx.console - - if cascade: - try: - if application_objects := self.get_objects_owned_by_application(): - application_objects_str = _application_objects_to_str( - application_objects + sql_executor = get_sql_executor() + with sql_executor.use_role(self.role): + if cascade: + try: + if application_objects := self.get_objects_owned_by_application(): + application_objects_str = _application_objects_to_str( + application_objects + ) + self.console.message( + f"The following objects are owned by application {self.name} and need to be dropped:\n{application_objects_str}" + ) + except ProgrammingError as err: + if err.errno != APPLICATION_NO_LONGER_AVAILABLE: + generic_sql_error_handler(err) + self.console.warning( + "The application owns other objects but they could not be determined." ) - console.message( - f"The following objects are owned by application {self.name} and need to be dropped:\n{application_objects_str}" + user_prompt = "Do you want the Snowflake CLI to drop these objects, then drop the existing application object and recreate it?" + else: + user_prompt = "Do you want the Snowflake CLI to drop the existing application object and recreate it?" + + if not policy.should_proceed(user_prompt): + if interactive: + self.console.message("Not upgrading the application object.") + raise typer.Exit(0) + else: + self.console.message( + "Cannot upgrade the application object non-interactively without --force." ) + raise typer.Exit(1) + try: + cascade_msg = " (cascade)" if cascade else "" + self.console.step( + f"Dropping application object {self.name}{cascade_msg}." + ) + cascade_sql = " cascade" if cascade else "" + sql_executor.execute_query(f"drop application {self.name}{cascade_sql}") except ProgrammingError as err: - if err.errno != APPLICATION_NO_LONGER_AVAILABLE: + if err.errno == APPLICATION_OWNS_EXTERNAL_OBJECTS and not cascade: + # We need to cascade the deletion, let's try again (only if we didn't try with cascade already) + return self.drop_application_before_upgrade( + policy=policy, + interactive=interactive, + cascade=True, + ) + else: generic_sql_error_handler(err) - console.warning( - "The application owns other objects but they could not be determined." - ) - user_prompt = "Do you want the Snowflake CLI to drop these objects, then drop the existing application object and recreate it?" - else: - user_prompt = "Do you want the Snowflake CLI to drop the existing application object and recreate it?" - - if not policy.should_proceed(user_prompt): - if interactive: - console.message("Not upgrading the application object.") - raise typer.Exit(0) - else: - console.message( - "Cannot upgrade the application object non-interactively without --force." - ) - raise typer.Exit(1) - try: - cascade_msg = " (cascade)" if cascade else "" - console.step(f"Dropping application object {self.name}{cascade_msg}.") - cascade_sql = " cascade" if cascade else "" - sql_executor = get_sql_executor() - sql_executor.execute_query(f"drop application {self.name}{cascade_sql}") - except ProgrammingError as err: - if err.errno == APPLICATION_OWNS_EXTERNAL_OBJECTS and not cascade: - # We need to cascade the deletion, let's try again (only if we didn't try with cascade already) - return self.drop_application_before_upgrade( - policy=policy, - interactive=interactive, - cascade=True, - ) - else: - generic_sql_error_handler(err) def get_events( self, diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py index 83f32d8b52..c61b7939dc 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py @@ -9,6 +9,7 @@ import typer from click import BadOptionUsage, ClickException from pydantic import Field, field_validator +from snowflake.cli._plugins.connection.util import UIParameter from snowflake.cli._plugins.nativeapp.artifacts import ( BundleMap, VersionInfo, @@ -25,7 +26,6 @@ NAME_COL, OWNER_COL, PATCH_COL, - SPECIAL_COMMENT, VERSION_COL, ) from snowflake.cli._plugins.nativeapp.exceptions import ( @@ -35,6 +35,7 @@ ObjectPropertyNotFoundError, SetupScriptFailedValidation, ) +from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag from snowflake.cli._plugins.nativeapp.policy import ( AllowAlwaysPolicy, AskAlwaysPolicy, @@ -824,6 +825,28 @@ def verify_project_distribution( return False return True + def _get_enable_release_channels_flag(self) -> Optional[bool]: + """ + Returns the requested value of enable_release_channels flag for the application package. + It retrieves the value from the configuration file and checks that the feature is enabled in the account. + If return value is None, it means do not explicitly set the flag. + """ + feature_flag_from_config = FeatureFlag.ENABLE_RELEASE_CHANNELS.get_value() + feature_enabled_in_account = ( + get_snowflake_facade().get_ui_parameter( + UIParameter.NA_FEATURE_RELEASE_CHANNELS, "ENABLED" + ) + == "ENABLED" + ) + + if feature_flag_from_config is not None and not feature_enabled_in_account: + self._workspace_ctx.console.warning( + f"Ignoring feature flag {FeatureFlag.ENABLE_RELEASE_CHANNELS.name} because release channels are not enabled in the current account." + ) + return None + + return feature_flag_from_config + def create_app_package(self) -> None: """ Creates the application package with our up-to-date stage if none exists. @@ -851,21 +874,23 @@ def create_app_package(self) -> None: if row_comment not in ALLOWED_SPECIAL_COMMENTS: raise ApplicationPackageAlreadyExistsError(self.name) + # 4. Update the application package with setting enable_release_channels if necessary + get_snowflake_facade().alter_application_package_properties( + package_name=self.name, + enable_release_channels=self._get_enable_release_channels_flag(), + role=self.role, + ) + return # If no application package pre-exists, create an application package, with the specified distribution in the project definition file. - sql_executor = get_sql_executor() - with sql_executor.use_role(self.role): - console.step(f"Creating new application package {self.name} in account.") - sql_executor.execute_query( - dedent( - f"""\ - create application package {self.name} - comment = {SPECIAL_COMMENT} - distribution = {model.distribution} - """ - ) - ) + console.step(f"Creating new application package {self.name} in account.") + get_snowflake_facade().create_application_package( + role=self.role, + enable_release_channels=self._get_enable_release_channels_flag(), + distribution=model.distribution, + package_name=self.name, + ) def execute_post_deploy_hooks(self): execute_post_deploy_hooks( diff --git a/src/snowflake/cli/_plugins/nativeapp/feature_flags.py b/src/snowflake/cli/_plugins/nativeapp/feature_flags.py index 14143619ea..dbc47e7483 100644 --- a/src/snowflake/cli/_plugins/nativeapp/feature_flags.py +++ b/src/snowflake/cli/_plugins/nativeapp/feature_flags.py @@ -22,3 +22,4 @@ class FeatureFlag(FeatureFlagMixin): ENABLE_NATIVE_APP_PYTHON_SETUP = BooleanFlag( "ENABLE_NATIVE_APP_PYTHON_SETUP", False ) + ENABLE_RELEASE_CHANNELS = BooleanFlag("ENABLE_RELEASE_CHANNELS", None) diff --git a/src/snowflake/cli/_plugins/nativeapp/same_account_install_method.py b/src/snowflake/cli/_plugins/nativeapp/same_account_install_method.py index 2664691f21..e4f34f0cb1 100644 --- a/src/snowflake/cli/_plugins/nativeapp/same_account_install_method.py +++ b/src/snowflake/cli/_plugins/nativeapp/same_account_install_method.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import Optional from snowflake.cli._plugins.nativeapp.constants import ( @@ -8,25 +9,15 @@ ApplicationCreatedExternallyError, ) from snowflake.cli._plugins.stage.manager import StageManager +from snowflake.cli.api.project.util import to_identifier +@dataclass class SameAccountInstallMethod: _requires_created_by_cli: bool - _from_release_directive: bool - version: Optional[str] - patch: Optional[int] - - def __init__( - self, - requires_created_by_cli: bool, - version: Optional[str] = None, - patch: Optional[int] = None, - from_release_directive: bool = False, - ): - self._requires_created_by_cli = requires_created_by_cli - self.version = version - self.patch = patch - self._from_release_directive = from_release_directive + version: Optional[str] = None + patch: Optional[int] = None + _from_release_directive: bool = False @classmethod def unversioned_dev(cls): @@ -39,7 +30,7 @@ def versioned_dev(cls, version: str, patch: Optional[int] = None): @classmethod def release_directive(cls): - return cls(False, from_release_directive=True) + return cls(False, _from_release_directive=True) @property def is_dev_mode(self) -> bool: @@ -53,8 +44,9 @@ def using_clause( return "" if self.version: + version_clause = f"version {to_identifier(self.version)}" patch_clause = f"patch {self.patch}" if self.patch else "" - return f"using version {self.version} {patch_clause}" + return f"using {version_clause} {patch_clause}" stage_name = StageManager.quote_stage_name(stage_fqn) return f"using {stage_name}" diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py index 1b0d7c722c..fcb2c88010 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py @@ -11,12 +11,83 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + from typing import NoReturn from click import ClickException from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType +from snowflake.cli.api.errno import ( + APPLICATION_FILE_NOT_FOUND_ON_STAGE, + APPLICATION_INSTANCE_EMPTY_SETUP_SCRIPT, + APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE, + APPLICATION_NO_LONGER_AVAILABLE, + APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS, + APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE, + APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND, + APPLICATION_PACKAGE_PATCH_DOES_NOT_EXIST, + CANNOT_GRANT_NON_MANIFEST_PRIVILEGE, + CANNOT_GRANT_OBJECT_NOT_IN_APP_PACKAGE, + CANNOT_GRANT_RESTRICTED_PRIVILEGE_TO_APP_PACKAGE_SHARE, + CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, + NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR, + NATIVE_APPLICATION_MANIFEST_INVALID_SYNTAX, + NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY, + NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD, + NO_REFERENCE_SET_FOR_DEFINITION, + NO_VERSIONS_AVAILABLE_FOR_ACCOUNT, + NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + ROLE_NOT_ASSIGNED, + SNOWSERVICES_IMAGE_MANIFEST_NOT_FOUND, + SNOWSERVICES_IMAGE_REPOSITORY_FAILS_TO_RETRIEVE_IMAGE_HASH_NEW, + SNOWSERVICES_IMAGE_REPOSITORY_IMAGE_IMPORT_TO_NATIVE_APP_FAIL, + VIEW_EXPANSION_FAILED, +) from snowflake.connector import DatabaseError, Error, ProgrammingError +# Reasons why an `alter application ... upgrade` might fail +UPGRADE_RESTRICTION_CODES = { + CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, + ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS, + APPLICATION_NO_LONGER_AVAILABLE, +} + +CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES = { + APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR, + APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE, + # when setup script/manifest/readme isn't on the stage + APPLICATION_FILE_NOT_FOUND_ON_STAGE, + NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD, + SNOWSERVICES_IMAGE_MANIFEST_NOT_FOUND, + # user tried to clone tables and it failed + VIEW_EXPANSION_FAILED, + # user tried to do something with a role that wasn't assigned to them + ROLE_NOT_ASSIGNED, + APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND, + SNOWSERVICES_IMAGE_REPOSITORY_IMAGE_IMPORT_TO_NATIVE_APP_FAIL, + APPLICATION_PACKAGE_PATCH_DOES_NOT_EXIST, + APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE, + SNOWSERVICES_IMAGE_REPOSITORY_FAILS_TO_RETRIEVE_IMAGE_HASH_NEW, + NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY, + CANNOT_GRANT_NON_MANIFEST_PRIVILEGE, + NO_REFERENCE_SET_FOR_DEFINITION, + NATIVE_APPLICATION_MANIFEST_INVALID_SYNTAX, + CANNOT_GRANT_OBJECT_NOT_IN_APP_PACKAGE, + APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND, + # user tried installing from release directive and there are none available + NO_VERSIONS_AVAILABLE_FOR_ACCOUNT, + APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE, + APPLICATION_INSTANCE_EMPTY_SETUP_SCRIPT, + APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS, + CANNOT_GRANT_RESTRICTED_PRIVILEGE_TO_APP_PACKAGE_SHARE, +} + def handle_unclassified_error(err: Error | Exception, context: str) -> NoReturn: """ @@ -115,3 +186,12 @@ def __init__( if role: message += f" using role: {role}" super().__init__(message) + + +class UpgradeApplicationRestrictionError(UserInputError): + """ + Raised when an alter application ... upgrade fails due to user error. + Must be caught and handled by the caller of an upgrade_application + """ + + pass diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index b8cd77dbee..e0fdbdb284 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -18,20 +18,39 @@ from textwrap import dedent from typing import Any, Dict, List +from snowflake.cli._plugins.connection.util import UIParameter, get_ui_parameter +from snowflake.cli._plugins.nativeapp.constants import ( + AUTHORIZE_TELEMETRY_COL, + NAME_COL, + SPECIAL_COMMENT, +) +from snowflake.cli._plugins.nativeapp.same_account_install_method import ( + SameAccountInstallMethod, +) from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import ( + CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES, + UPGRADE_RESTRICTION_CODES, CouldNotUseObjectError, InsufficientPrivilegesError, UnexpectedResultError, + UpgradeApplicationRestrictionError, + UserInputError, UserScriptError, handle_unclassified_error, ) +from snowflake.cli.api.cli_global_context import get_cli_context +from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.errno import ( + APPLICATION_REQUIRES_TELEMETRY_SHARING, + CANNOT_DISABLE_MANDATORY_TELEMETRY, DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, ) from snowflake.cli.api.identifiers import FQN +from snowflake.cli.api.metrics import CLICounterField +from snowflake.cli.api.project.schemas.v1.native_app.package import DistributionOptions from snowflake.cli.api.project.util import ( identifier_to_show_like_pattern, is_valid_unquoted_identifier, @@ -39,12 +58,13 @@ to_quoted_identifier, to_string_literal, ) -from snowflake.cli.api.sql_execution import BaseSqlExecutor, SqlExecutor +from snowflake.cli.api.sql_execution import BaseSqlExecutor +from snowflake.cli.api.utils.cursor import find_first_row from snowflake.connector import DictCursor, ProgrammingError class SnowflakeSQLFacade: - def __init__(self, sql_executor: SqlExecutor | None = None): + def __init__(self, sql_executor: BaseSqlExecutor | None = None): self._sql_executor = ( sql_executor if sql_executor is not None else BaseSqlExecutor() ) @@ -136,6 +156,37 @@ def _use_schema_optional(self, schema_name: str | None): """ return self._use_object_optional(UseObjectType.SCHEMA, schema_name) + def grant_privileges_to_role( + self, + privileges: list[str], + object_type: ObjectType, + object_identifier: str, + role_to_grant: str, + role_to_use: str | None = None, + ) -> None: + """ + Grants one or more access privileges on a securable object to a role + + @param privileges: List of privileges to grant to a role + @param object_type: Type of snowflake object to grant to a role + @param object_identifier: Valid identifier of the snowflake object to grant to a role + @param role_to_grant: Name of the role to grant privileges to + @param [Optional] role_to_use: Name of the role to use to grant privileges + """ + comma_separated_privileges = ", ".join(privileges) + object_type_and_name = f"{object_type.value.sf_name} {object_identifier}" + + with self._use_role_optional(role_to_use): + try: + self._sql_executor.execute_query( + f"grant {comma_separated_privileges} on {object_type_and_name} to role {role_to_grant}" + ) + except Exception as err: + handle_unclassified_error( + err, + f"Failed to grant {comma_separated_privileges} on {object_type_and_name} to role {role_to_grant}.", + ) + def execute_user_script( self, queries: str, @@ -503,6 +554,291 @@ def show_release_directives( ) return cursor.fetchall() + def get_existing_app_info(self, name: str, role: str) -> dict | None: + """ + Check for an existing application object by the same name as in project definition, in account. + It executes a 'show applications like' query and returns the result as single row, if one exists. + """ + with self._use_role_optional(role): + try: + object_type_plural = ObjectType.APPLICATION.value.sf_plural_name + show_obj_query = f"show {object_type_plural} like {identifier_to_show_like_pattern(name)}".strip() + + show_obj_cursor = self._sql_executor.execute_query( + show_obj_query, cursor_class=DictCursor + ) + + show_obj_row = find_first_row( + show_obj_cursor, lambda row: _same_identifier(row[NAME_COL], name) + ) + except Exception as err: + handle_unclassified_error( + err, f"Unable to fetch information on application {name}." + ) + return show_obj_row + + def upgrade_application( + self, + name: str, + install_method: SameAccountInstallMethod, + stage_fqn: str, + role: str, + warehouse: str, + debug_mode: bool | None, + should_authorize_event_sharing: bool | None, + ) -> list[tuple[str]]: + """ + Upgrades an application object using the provided clauses + + @param name: Name of the application object + @param install_method: Method of installing the application + @param stage_fqn: FQN of the stage housing the application artifacts + @param role: Role to use when creating the application and provider-side objects + @param warehouse: Warehouse which is required to create an application object + @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled + @param should_authorize_event_sharing: Whether to enable event sharing; None means not explicitly enabled or disabled + """ + install_method.ensure_app_usable( + app_name=name, + app_role=role, + show_app_row=self.get_existing_app_info(name, role), + ) + # If all the above checks are in order, proceed to upgrade + + with self._use_role_optional(role), self._use_warehouse_optional(warehouse): + try: + using_clause = install_method.using_clause(stage_fqn) + upgrade_cursor = self._sql_executor.execute_query( + f"alter application {name} upgrade {using_clause}", + ) + + # if debug_mode is present (controlled), ensure it is up-to-date + if install_method.is_dev_mode: + if debug_mode is not None: + self._sql_executor.execute_query( + f"alter application {name} set debug_mode = {debug_mode}" + ) + except ProgrammingError as err: + if err.errno in UPGRADE_RESTRICTION_CODES: + raise UpgradeApplicationRestrictionError(err.msg) from err + elif ( + err.errno in CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES + ): + raise UserInputError( + f"Failed to upgrade application {name} with the following error message:\n" + f"{err.msg}" + ) from err + handle_unclassified_error(err, f"Failed to upgrade application {name}.") + except Exception as err: + handle_unclassified_error(err, f"Failed to upgrade application {name}.") + + try: + # Only update event sharing if the current value is different as the one we want to set + if should_authorize_event_sharing is not None: + current_authorize_event_sharing = ( + self.get_app_properties(name, role) + .get(AUTHORIZE_TELEMETRY_COL, "false") + .lower() + == "true" + ) + if ( + current_authorize_event_sharing + != should_authorize_event_sharing + ): + self._log.info( + "Setting telemetry sharing authorization to %s", + should_authorize_event_sharing, + ) + self._sql_executor.execute_query( + f"alter application {name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}" + ) + except ProgrammingError as err: + if err.errno == CANNOT_DISABLE_MANDATORY_TELEMETRY: + get_cli_context().metrics.set_counter( + CLICounterField.EVENT_SHARING_ERROR, 1 + ) + raise UserInputError( + "Could not disable telemetry event sharing for the application because it contains mandatory events. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ) from err + handle_unclassified_error( + err, + f"Failed to set AUTHORIZE_TELEMETRY_EVENT_SHARING when upgrading application {name}.", + ) + except Exception as err: + handle_unclassified_error( + err, + f"Failed to set AUTHORIZE_TELEMETRY_EVENT_SHARING when upgrading application {name}.", + ) + + return upgrade_cursor.fetchall() + + def create_application( + self, + name: str, + package_name: str, + install_method: SameAccountInstallMethod, + stage_fqn: str, + role: str, + warehouse: str, + debug_mode: bool | None, + should_authorize_event_sharing: bool | None, + ) -> list[tuple[str]]: + """ + Creates a new application object using an application package, + running the setup script of the application package + + @param name: Name of the application object + @param package_name: Name of the application package to install the application from + @param install_method: Method of installing the application + @param stage_fqn: FQN of the stage housing the application artifacts + @param role: Role to use when creating the application and provider-side objects + @param warehouse: Warehouse which is required to create an application object + @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled + @param should_authorize_event_sharing: Whether to enable event sharing; None means not explicitly enabled or disabled + """ + + # by default, applications are created in debug mode when possible; + # this can be overridden in the project definition + debug_mode_clause = "" + if install_method.is_dev_mode: + initial_debug_mode = debug_mode if debug_mode is not None else True + debug_mode_clause = f"debug_mode = {initial_debug_mode}" + + authorize_telemetry_clause = "" + if should_authorize_event_sharing is not None: + self._log.info( + "Setting AUTHORIZE_TELEMETRY_EVENT_SHARING to %s", + should_authorize_event_sharing, + ) + authorize_telemetry_clause = f" AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(should_authorize_event_sharing).upper()}" + + using_clause = install_method.using_clause(stage_fqn) + with self._use_role_optional(role), self._use_warehouse_optional(warehouse): + try: + create_cursor = self._sql_executor.execute_query( + dedent( + f"""\ + create application {name} + from application package {package_name} {using_clause} {debug_mode_clause}{authorize_telemetry_clause} + comment = {SPECIAL_COMMENT} + """ + ), + ) + except ProgrammingError as err: + if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING: + get_cli_context().metrics.set_counter( + CLICounterField.EVENT_SHARING_ERROR, 1 + ) + raise UserInputError( + "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ) from err + elif ( + err.errno in CREATE_OR_UPGRADE_APPLICATION_EXPECTED_USER_ERROR_CODES + ): + raise UserInputError( + f"Failed to create application {name} with the following error message:\n" + f"{err.msg}" + ) from err + handle_unclassified_error(err, f"Failed to create application {name}.") + except Exception as err: + handle_unclassified_error(err, f"Failed to create application {name}.") + + return create_cursor.fetchall() + + def create_application_package( + self, + package_name: str, + distribution: DistributionOptions, + enable_release_channels: bool | None = None, + role: str | None = None, + ) -> None: + """ + Creates a new application package. + @param package_name: Name of the application package to create. + @param [Optional] enable_release_channels: Enable/Disable release channels if not None. + @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in. + """ + package_name = to_identifier(package_name) + + enable_release_channels_clause = "" + if enable_release_channels is not None: + enable_release_channels_clause = ( + f"enable_release_channels = {str(enable_release_channels).lower()}" + ) + + with self._use_role_optional(role): + try: + self._sql_executor.execute_query( + dedent( + _strip_empty_lines( + f"""\ + create application package {package_name} + comment = {SPECIAL_COMMENT} + distribution = {distribution} + {enable_release_channels_clause} + """ + ) + ) + ) + except ProgrammingError as err: + if err.errno == INSUFFICIENT_PRIVILEGES: + raise InsufficientPrivilegesError( + f"Insufficient privileges to create application package {package_name}", + role=role, + ) from err + handle_unclassified_error( + err, f"Failed to create application package {package_name}." + ) + + def alter_application_package_properties( + self, + package_name: str, + enable_release_channels: bool | None = None, + role: str | None = None, + ) -> None: + """ + Alters the properties of an existing application package. + @param package_name: Name of the application package to alter. + @param [Optional] enable_release_channels: Enable/Disable release channels if not None. + @param [Optional] role: Role to switch to while running this script. Current role will be used if no role is passed in. + """ + + package_name = to_identifier(package_name) + + if enable_release_channels is not None: + with self._use_role_optional(role): + try: + self._sql_executor.execute_query( + dedent( + f"""\ + alter application package {package_name} + set enable_release_channels = {str(enable_release_channels).lower()} + """ + ) + ) + except ProgrammingError as err: + if err.errno == INSUFFICIENT_PRIVILEGES: + raise InsufficientPrivilegesError( + f"Insufficient privileges update enable_release_channels for application package {package_name}", + role=role, + ) from err + handle_unclassified_error( + err, + f"Failed to update enable_release_channels for application package {package_name}.", + ) + + def get_ui_parameter(self, parameter: UIParameter, default: Any) -> Any: + """ + Returns the value of a single UI parameter. + If the parameter is not found, the default value is returned. + + @param parameter: UIParameter, the parameter to get the value of. + @param default: Default value to return if the parameter is not found. + """ + connection = self._sql_executor._conn # noqa SLF001 + + return get_ui_parameter(connection, parameter, default) + # TODO move this to src/snowflake/cli/api/project/util.py in a separate # PR since it's codeowned by the CLI team @@ -523,3 +859,10 @@ def _same_identifier(id1: str, id2: str) -> bool: # The canonical identifiers are equal if they are equal when both are quoted # (if they are already quoted, this is a no-op) return to_quoted_identifier(canonical_id1) == to_quoted_identifier(canonical_id2) + + +def _strip_empty_lines(text: str) -> str: + """ + Strips empty lines from the input string. + """ + return "\n".join(line for line in text.splitlines() if line.strip()) diff --git a/src/snowflake/cli/_plugins/stage/manager.py b/src/snowflake/cli/_plugins/stage/manager.py index dbeab38d94..71b4e18cdd 100644 --- a/src/snowflake/cli/_plugins/stage/manager.py +++ b/src/snowflake/cli/_plugins/stage/manager.py @@ -41,7 +41,7 @@ from snowflake.cli.api.console import cli_console from snowflake.cli.api.constants import PYTHON_3_12 from snowflake.cli.api.identifiers import FQN -from snowflake.cli.api.project.util import to_string_literal +from snowflake.cli.api.project.util import extract_schema, to_string_literal from snowflake.cli.api.secure_path import SecurePath from snowflake.cli.api.sql_execution import SqlExecutionMixin from snowflake.cli.api.stage_path import StagePath @@ -86,6 +86,10 @@ def path(self) -> str: def full_path(self) -> str: raise NotImplementedError + @property + def schema(self) -> str | None: + raise NotImplementedError + def replace_stage_prefix(self, file_path: str) -> str: raise NotImplementedError @@ -139,11 +143,15 @@ def __init__(self, stage_path: str): @property def path(self) -> str: - return f"{self.stage_name.rstrip('/')}/{self.directory}" + return f"{self.stage_name.rstrip('/')}/{self.directory}".rstrip("/") @property def full_path(self) -> str: - return f"{self.stage.rstrip('/')}/{self.directory}" + return f"{self.stage.rstrip('/')}/{self.directory}".rstrip("/") + + @property + def schema(self) -> str | None: + return extract_schema(self.stage) def replace_stage_prefix(self, file_path: str) -> str: stage = Path(self.stage).parts[0] @@ -193,7 +201,7 @@ def path(self) -> str: @property def full_path(self) -> str: - return f"{self.stage}/{self.directory}" + return f"{self.stage}/{self.directory}".rstrip("/") def replace_stage_prefix(self, file_path: str) -> str: if Path(file_path).parts[0] == self.stage_name: diff --git a/src/snowflake/cli/api/config.py b/src/snowflake/cli/api/config.py index b6e26a68d7..0a03c36b97 100644 --- a/src/snowflake/cli/api/config.py +++ b/src/snowflake/cli/api/config.py @@ -286,8 +286,12 @@ def get_config_value(*path, key: str, default: Optional[Any] = Empty) -> Any: raise -def get_config_bool_value(*path, key: str, default: Optional[Any] = Empty) -> bool: - value = get_config_value(*path, key=key, default=default) +def get_config_bool_value(*path, key: str, default: Optional[bool]) -> Optional[bool]: + value = get_config_value(*path, key=key, default=None) + + if value is None: + return default + try: return try_cast_to_bool(value) except ValueError: diff --git a/src/snowflake/cli/api/constants.py b/src/snowflake/cli/api/constants.py index 2e16e9abdd..d6a3e27f6e 100644 --- a/src/snowflake/cli/api/constants.py +++ b/src/snowflake/cli/api/constants.py @@ -62,6 +62,10 @@ class ObjectType(Enum): "image-repository", "image repository", "image repositories" ) GIT_REPOSITORY = ObjectNames("git-repository", "git repository", "git repositories") + APPLICATION = ObjectNames("application", "application", "applications") + APPLICATION_PACKAGE = ObjectNames( + "application-package", "application package", "application packages" + ) def __str__(self): """This makes using this Enum easier in formatted string""" @@ -69,7 +73,11 @@ def __str__(self): OBJECT_TO_NAMES = {o.value.cli_name: o.value for o in ObjectType} -SUPPORTED_OBJECTS = sorted(OBJECT_TO_NAMES.keys()) +UNSUPPORTED_OBJECTS = { + ObjectType.APPLICATION.value.cli_name, + ObjectType.APPLICATION_PACKAGE.value.cli_name, +} +SUPPORTED_OBJECTS = sorted(OBJECT_TO_NAMES.keys() - UNSUPPORTED_OBJECTS) # Scope names here must replace spaces with '-'. For example 'compute pool' is 'compute-pool'. VALID_SCOPES = ["database", "schema", "compute-pool"] diff --git a/src/snowflake/cli/api/entities/utils.py b/src/snowflake/cli/api/entities/utils.py index 4c5b8b0c78..400fe726f4 100644 --- a/src/snowflake/cli/api/entities/utils.py +++ b/src/snowflake/cli/api/entities/utils.py @@ -42,7 +42,6 @@ ) from snowflake.cli.api.secure_path import UNLIMITED, SecurePath from snowflake.connector import ProgrammingError -from snowflake.connector.cursor import SnowflakeCursor def generic_sql_error_handler(err: ProgrammingError) -> NoReturn: @@ -325,17 +324,15 @@ def drop_generic_object( console.message(f"Dropped {object_type} {object_name} successfully.") -def print_messages( - console: AbstractConsole, create_or_upgrade_cursor: Optional[SnowflakeCursor] -): +def print_messages(console: AbstractConsole, cursor_results: list[tuple[str]]): """ Shows messages in the console returned by the CREATE or UPGRADE APPLICATION command. """ - if not create_or_upgrade_cursor: + if not cursor_results: return - messages = [row[0] for row in create_or_upgrade_cursor.fetchall()] + messages = [row[0] for row in cursor_results] for message in messages: console.warning(message) console.message("") diff --git a/src/snowflake/cli/api/errno.py b/src/snowflake/cli/api/errno.py index bfe4942992..c49fd167fa 100644 --- a/src/snowflake/cli/api/errno.py +++ b/src/snowflake/cli/api/errno.py @@ -14,17 +14,59 @@ # General errors NO_WAREHOUSE_SELECTED_IN_SESSION = 606 +EMPTY_SQL_STATEMENT = 900 -DOES_NOT_EXIST_OR_NOT_AUTHORIZED = 2003 -DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED = 2043 -INSUFFICIENT_PRIVILEGES = 3001 +SQL_COMPILATION_ERROR = 1003 +OBJECT_ALREADY_EXISTS_IN_DOMAIN = 1998 +OBJECT_ALREADY_EXISTS = 2002 +DOES_NOT_EXIST_OR_NOT_AUTHORIZED = 2003 # BASE_TABLE_OR_VIEW_NOT_FOUND +DUPLICATE_COLUMN_NAME = 2025 +VIEW_EXPANSION_FAILED = 2037 +DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED = ( + 2043 # OBJECT_DOES_NOT_EXIST_OR_CANNOT_PERFORM_OPERATION +) +INSUFFICIENT_PRIVILEGES = 3001 # NOT_AUTHORIZED +INVALID_OBJECT_TYPE_FOR_SPECIFIED_PRIVILEGE = 3008 +ROLE_NOT_ASSIGNED = 3013 +NO_INDIVIDUAL_PRIVS = 3028 +OBJECT_ALREADY_EXISTS_NO_PRIVILEGES = 3041 # Native Apps +APPLICATION_PACKAGE_MANIFEST_SPECIFIED_FILE_NOT_FOUND = 93003 +APPLICATION_FILE_NOT_FOUND_ON_STAGE = 93009 +CANNOT_GRANT_OBJECT_NOT_IN_APP_PACKAGE = 93011 +CANNOT_GRANT_RESTRICTED_PRIVILEGE_TO_APP_PACKAGE_SHARE = 93012 +APPLICATION_PACKAGE_VERSION_ALREADY_EXISTS = 93030 +APPLICATION_PACKAGE_VERSION_NAME_TOO_LONG = 93035 +APPLICATION_PACKAGE_PATCH_DOES_NOT_EXIST = 93036 +APPLICATION_PACKAGE_MAX_VERSIONS_HIT = 93037 CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION = 93044 CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES = 93045 ONLY_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93046 +NO_VERSIONS_AVAILABLE_FOR_ACCOUNT = 93054 NOT_SUPPORTED_ON_DEV_MODE_APPLICATIONS = 93055 APPLICATION_NO_LONGER_AVAILABLE = 93079 +APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT = 93082 +APPLICATION_INSTANCE_NO_ACTIVE_WAREHOUSE_FOR_CREATE_OR_UPGRADE = 93083 +APPLICATION_INSTANCE_EMPTY_SETUP_SCRIPT = 93084 +APPLICATION_PACKAGE_CANNOT_DROP_VERSION_IF_IT_IS_IN_USE = 93088 +APPLICATION_PACKAGE_MANIFEST_CONTAINER_IMAGE_URL_BAD_VALUE = 93148 +CANNOT_GRANT_NON_MANIFEST_PRIVILEGE = 93118 APPLICATION_OWNS_EXTERNAL_OBJECTS = 93128 +APPLICATION_PACKAGE_PATCH_ALREADY_EXISTS = 93168 +APPLICATION_PACKAGE_CANNOT_SET_EXTERNAL_DISTRIBUTION_WITH_SPCS = 93197 +NATIVE_APPLICATION_MANIFEST_UNRECOGNIZED_FIELD = 93301 +NATIVE_APPLICATION_MANIFEST_UNEXPECTED_VALUE_FOR_PROPERTY = 93302 +NATIVE_APPLICATION_MANIFEST_GENERIC_JSON_ERROR = 93303 +NATIVE_APPLICATION_MANIFEST_INVALID_SYNTAX = 93300 APPLICATION_REQUIRES_TELEMETRY_SHARING = 93321 CANNOT_DISABLE_MANDATORY_TELEMETRY = 93329 + +ERR_JAVASCRIPT_EXECUTION = 100132 + +SNOWSERVICES_IMAGE_REPOSITORY_IMAGE_IMPORT_TO_NATIVE_APP_FAIL = 397007 +SNOWSERVICES_IMAGE_MANIFEST_NOT_FOUND = 397012 +SNOWSERVICES_IMAGE_REPOSITORY_FAILS_TO_RETRIEVE_IMAGE_HASH_NEW = 397013 + +NO_REFERENCE_SET_FOR_DEFINITION = 505019 +NO_ACTIVE_REF_DEFINITION_WITH_REF_NAME_IN_APPLICATION = 505026 diff --git a/src/snowflake/cli/api/feature_flags.py b/src/snowflake/cli/api/feature_flags.py index 2ed9728e55..d504056e02 100644 --- a/src/snowflake/cli/api/feature_flags.py +++ b/src/snowflake/cli/api/feature_flags.py @@ -24,20 +24,31 @@ class BooleanFlag(NamedTuple): name: str - default: bool = False + default: bool | None = False @unique class FeatureFlagMixin(Enum): - def is_enabled(self) -> bool: + def get_value(self) -> bool | None: return get_config_bool_value( *FEATURE_FLAGS_SECTION_PATH, key=self.value.name.lower(), default=self.value.default, ) - def is_disabled(self): - return not self.is_enabled() + def is_enabled(self) -> bool: + return self.get_value() is True + + def is_disabled(self) -> bool: + return self.get_value() is False + + def is_set(self) -> bool: + return ( + get_config_bool_value( + *FEATURE_FLAGS_SECTION_PATH, key=self.value.name.lower(), default=None + ) + is not None + ) def env_variable(self): return get_env_variable_name(*FEATURE_FLAGS_SECTION_PATH, key=self.value.name) diff --git a/src/snowflake/cli/api/project/definition_conversion.py b/src/snowflake/cli/api/project/definition_conversion.py index 1f76e998d2..7834586a07 100644 --- a/src/snowflake/cli/api/project/definition_conversion.py +++ b/src/snowflake/cli/api/project/definition_conversion.py @@ -13,7 +13,6 @@ bundle_artifacts, ) from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext -from snowflake.cli._plugins.nativeapp.codegen.compiler import TEMPLATES_PROCESSOR from snowflake.cli._plugins.nativeapp.codegen.templates.templates_processor import ( TemplatesProcessor, ) @@ -457,7 +456,7 @@ def _convert_templates_in_files( artifact for artifact in pkg_model.artifacts for processor in artifact.processors - if processor.name == TEMPLATES_PROCESSOR + if processor.name.lower() == TemplatesProcessor.NAME ] if not in_memory and artifacts_to_template: metrics.set_counter(CLICounterField.TEMPLATES_PROCESSOR, 1) diff --git a/tests/api/test_feature_flags.py b/tests/api/test_feature_flags.py index 0b41a3626f..d4d9da8e9c 100644 --- a/tests/api/test_feature_flags.py +++ b/tests/api/test_feature_flags.py @@ -15,7 +15,6 @@ from unittest import mock import pytest -from click import ClickException from snowflake.cli.api.feature_flags import BooleanFlag, FeatureFlagMixin @@ -23,22 +22,36 @@ class _TestFlags(FeatureFlagMixin): # Intentional inconsistency between constant and the enum name to make sure there's no strict relation ENABLED_BY_DEFAULT = BooleanFlag("ENABLED_DEFAULT", True) DISABLED_BY_DEFAULT = BooleanFlag("DISABLED_DEFAULT", False) - NON_BOOLEAN = BooleanFlag("NON_BOOLEAN", "xys") # type: ignore + NON_BOOLEAN_DEFAULT = BooleanFlag("NON_BOOLEAN", "xys") # type: ignore + NONE_AS_DEFAULT = BooleanFlag("NON_BOOLEAN", "xys") # type: ignore -def test_flag_value_has_to_be_boolean(): - with pytest.raises(ClickException): - _TestFlags.NON_BOOLEAN.is_enabled() +def test_flag_value_default_non_boolean(): + _TestFlags.NON_BOOLEAN_DEFAULT.is_enabled() is False + _TestFlags.NON_BOOLEAN_DEFAULT.is_disabled() is False + _TestFlags.NON_BOOLEAN_DEFAULT.get_value() == "xys" + _TestFlags.NON_BOOLEAN_DEFAULT.is_set() is True + + +def test_flag_value_default_is_none(): + _TestFlags.NONE_AS_DEFAULT.is_enabled() is False + _TestFlags.NONE_AS_DEFAULT.is_disabled() is False + _TestFlags.NONE_AS_DEFAULT.get_value() is None + _TestFlags.NONE_AS_DEFAULT.is_set() is False def test_flag_is_enabled(): assert _TestFlags.ENABLED_BY_DEFAULT.is_enabled() is True assert _TestFlags.ENABLED_BY_DEFAULT.is_disabled() is False + assert _TestFlags.ENABLED_BY_DEFAULT.get_value() is True + assert _TestFlags.ENABLED_BY_DEFAULT.is_set() is False def test_flag_is_disabled(): assert _TestFlags.DISABLED_BY_DEFAULT.is_enabled() is False assert _TestFlags.DISABLED_BY_DEFAULT.is_disabled() is True + assert _TestFlags.DISABLED_BY_DEFAULT.get_value() is False + assert _TestFlags.DISABLED_BY_DEFAULT.is_set() is False def test_flag_env_variable_value(): @@ -53,13 +66,50 @@ def test_flag_env_variable_value(): @mock.patch("snowflake.cli.api.config.get_config_value") -@pytest.mark.parametrize("value_from_config", [True, False]) -def test_flag_from_config_file(mock_get_config_value, value_from_config): +@pytest.mark.parametrize("value_from_config", [True, False, None]) +def test_is_enabled_flag_from_config_file(mock_get_config_value, value_from_config): + mock_get_config_value.return_value = value_from_config + + assert _TestFlags.DISABLED_BY_DEFAULT.is_enabled() is (value_from_config or False) + mock_get_config_value.assert_called_once_with( + "cli", "features", key="disabled_default", default=None + ) + + +@mock.patch("snowflake.cli.api.config.get_config_value") +@pytest.mark.parametrize("value_from_config", [True, False, None]) +def test_is_disabled_flag_from_config_file(mock_get_config_value, value_from_config): + mock_get_config_value.return_value = value_from_config + + assert _TestFlags.DISABLED_BY_DEFAULT.is_disabled() is not ( + value_from_config or False + ) + mock_get_config_value.assert_called_once_with( + "cli", "features", key="disabled_default", default=None + ) + + +@mock.patch("snowflake.cli.api.config.get_config_value") +@pytest.mark.parametrize("value_from_config", [True, False, None]) +def test_is_set_flag_from_config_file(mock_get_config_value, value_from_config): mock_get_config_value.return_value = value_from_config - assert _TestFlags.DISABLED_BY_DEFAULT.is_enabled() is value_from_config + assert _TestFlags.DISABLED_BY_DEFAULT.is_set() is (value_from_config is not None) + + mock_get_config_value.assert_called_once_with( + "cli", "features", key="disabled_default", default=None + ) + + +@mock.patch("snowflake.cli.api.config.get_config_value") +@pytest.mark.parametrize("value_from_config", [True, False, None]) +def test_get_value_flag_from_config_file(mock_get_config_value, value_from_config): + mock_get_config_value.return_value = value_from_config + + assert _TestFlags.DISABLED_BY_DEFAULT.get_value() == (value_from_config or False) + mock_get_config_value.assert_called_once_with( - "cli", "features", key="disabled_default", default=False + "cli", "features", key="disabled_default", default=None ) diff --git a/tests/nativeapp/codegen/templating/test_templates_processor.py b/tests/nativeapp/codegen/templating/test_templates_processor.py index 21c7160d06..65eb5b3dac 100644 --- a/tests/nativeapp/codegen/templating/test_templates_processor.py +++ b/tests/nativeapp/codegen/templating/test_templates_processor.py @@ -28,7 +28,10 @@ from snowflake.cli.api.exceptions import InvalidTemplate from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping -from tests.nativeapp.utils import CLI_GLOBAL_TEMPLATE_CONTEXT +from tests.nativeapp.utils import ( + CLI_GLOBAL_TEMPLATE_CONTEXT, + TEMPLATE_PROCESSOR, +) @dataclass @@ -213,3 +216,24 @@ def test_file_with_undefined_variable(): assert "does not contain a valid template" in str(e.value) assert bundle_result.output_files[0].is_symlink() assert bundle_result.output_files[0].read_text() == file_contents[0] + + +@mock.patch(CLI_GLOBAL_TEMPLATE_CONTEXT, {}) +@mock.patch(f"{TEMPLATE_PROCESSOR}.cc.warning") +def test_expand_templates_in_file_unicode_decode_error(mock_cc_warning): + file_name = ["test_file.txt"] + file_contents = ["This is a test file"] + with TemporaryDirectory() as tmp_dir: + bundle_result = bundle_files(tmp_dir, file_name, file_contents) + templates_processor = TemplatesProcessor(bundle_ctx=bundle_result.bundle_ctx) + with mock.patch( + f"{TEMPLATE_PROCESSOR}.TemplatesProcessor.edit_file", + side_effect=UnicodeDecodeError("utf-8", b"", 0, 1, "invalid start byte"), + ): + src_path = Path( + bundle_result.bundle_ctx.project_root / "src" / file_name[0] + ).relative_to(bundle_result.bundle_ctx.project_root) + templates_processor.process(bundle_result.artifact_to_process, None) + mock_cc_warning.assert_called_once_with( + f"Could not read file {src_path}, error: invalid start byte. Skipping this file." + ) diff --git a/tests/nativeapp/codegen/test_compiler.py b/tests/nativeapp/codegen/test_compiler.py index 4ec64ef983..7da3382588 100644 --- a/tests/nativeapp/codegen/test_compiler.py +++ b/tests/nativeapp/codegen/test_compiler.py @@ -13,10 +13,12 @@ # limitations under the License. import re from pathlib import Path +from typing import Optional import pytest from snowflake.cli._plugins.nativeapp.bundle_context import BundleContext from snowflake.cli._plugins.nativeapp.codegen.artifact_processor import ( + ArtifactProcessor, UnsupportedArtifactProcessorError, ) from snowflake.cli._plugins.nativeapp.codegen.compiler import NativeAppCompiler @@ -29,6 +31,10 @@ from snowflake.cli.api.project.schemas.project_definition import ( build_project_definition, ) +from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import ( + PathMapping, + ProcessorMapping, +) @pytest.fixture() @@ -114,3 +120,35 @@ def test_find_and_execute_processors_exception(test_proj_def, test_compiler): with pytest.raises(UnsupportedArtifactProcessorError): test_compiler.compile_artifacts() + + +class TestProcessor(ArtifactProcessor): + NAME = "test_processor" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + assert False # never invoked + + @staticmethod + def is_enabled(): + return False + + def process( + self, + artifact_to_process: PathMapping, + processor_mapping: Optional[ProcessorMapping], + **kwargs, + ) -> None: + assert False # never invoked + + +def test_skips_disabled_processors(test_proj_def, test_compiler): + pkg_model = test_proj_def.entities["pkg"] + pkg_model.artifacts = [ + {"dest": "./", "src": "app/*", "processors": ["test_processor"]} + ] + test_compiler = NativeAppCompiler(_get_bundle_context(pkg_model)) + test_compiler.register(TestProcessor) + + # TestProcessor is never invoked, otherwise calling its methods will make the test fail + test_compiler.compile_artifacts() diff --git a/tests/nativeapp/test_application_package_entity.py b/tests/nativeapp/test_application_package_entity.py index 3966a391ff..d07683ac94 100644 --- a/tests/nativeapp/test_application_package_entity.py +++ b/tests/nativeapp/test_application_package_entity.py @@ -17,6 +17,7 @@ from unittest import mock import yaml +from snowflake.cli._plugins.connection.util import UIParameter from snowflake.cli._plugins.nativeapp.constants import ( LOOSE_FILES_MAGIC_VERSION, SPECIAL_COMMENT, @@ -32,6 +33,7 @@ APP_PACKAGE_ENTITY, APPLICATION_PACKAGE_ENTITY_MODULE, SQL_EXECUTOR_EXECUTE, + SQL_FACADE_GET_UI_PARAMETER, mock_execute_helper, ) @@ -75,7 +77,9 @@ def test_bundle(project_directory): @mock.patch(f"{APP_PACKAGE_ENTITY}.execute_post_deploy_hooks") @mock.patch(f"{APP_PACKAGE_ENTITY}.validate_setup_script") @mock.patch(f"{APPLICATION_PACKAGE_ENTITY_MODULE}.sync_deploy_root_with_stage") +@mock.patch(SQL_FACADE_GET_UI_PARAMETER, return_value="ENABLED") def test_deploy( + mock_get_parameter, mock_sync, mock_validate, mock_execute_post_deploy_hooks, @@ -164,6 +168,9 @@ def test_deploy( ) mock_validate.assert_called_once() mock_execute_post_deploy_hooks.assert_called_once_with() + mock_get_parameter.assert_called_once_with( + UIParameter.NA_FEATURE_RELEASE_CHANNELS, "ENABLED" + ) assert mock_execute.mock_calls == expected diff --git a/tests/nativeapp/test_event_sharing.py b/tests/nativeapp/test_event_sharing.py index c26285f9b8..8da67f6c29 100644 --- a/tests/nativeapp/test_event_sharing.py +++ b/tests/nativeapp/test_event_sharing.py @@ -19,9 +19,6 @@ import pytest from click import ClickException from snowflake.cli._plugins.connection.util import UIParameter -from snowflake.cli._plugins.nativeapp.constants import ( - SPECIAL_COMMENT, -) from snowflake.cli._plugins.nativeapp.entities.application import ( ApplicationEntity, ApplicationEntityModel, @@ -39,9 +36,11 @@ from snowflake.cli._plugins.nativeapp.same_account_install_method import ( SameAccountInstallMethod, ) +from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import UserInputError from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext from snowflake.cli.api.console import cli_console as cc from snowflake.cli.api.console.abc import AbstractConsole +from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.errno import ( APPLICATION_REQUIRES_TELEMETRY_SHARING, CANNOT_DISABLE_MANDATORY_TELEMETRY, @@ -59,13 +58,23 @@ mock_connection, ) from tests.nativeapp.utils import ( - APP_ENTITY_GET_EXISTING_APP_INFO, GET_UI_PARAMETERS, SQL_EXECUTOR_EXECUTE, + SQL_FACADE_CREATE_APPLICATION, + SQL_FACADE_GET_EXISTING_APP_INFO, + SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE, + SQL_FACADE_UPGRADE_APPLICATION, mock_execute_helper, + mock_side_effect_error_with_cause, ) from tests.testing_utils.fixtures import MockConnectionCtx +DEFAULT_APP_ID = "myapp" +DEFAULT_PKG_ID = "app_pkg" +DEFAULT_STAGE_FQN = "app_pkg.app_src.stage" +DEFAULT_SUCCESS_MESSAGE = "Application successfully upgraded." +DEFAULT_USER_INPUT_ERROR_MESSAGE = "User input error message." + allow_always_policy = AllowAlwaysPolicy() ask_always_policy = AskAlwaysPolicy() deny_always_policy = DenyAlwaysPolicy() @@ -187,6 +196,8 @@ def _setup_project( def _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -194,24 +205,24 @@ def _setup_mocks_for_app( expected_shared_events=None, is_prod=False, is_upgrade=False, - existing_app_flag=False, events_definitions_in_app=None, - programming_errno=None, + error_raised=None, ): if is_upgrade: return _setup_mocks_for_upgrade_app( + mock_sql_facade_upgrade_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, expected_authorize_telemetry_flag=expected_authorize_telemetry_flag, expected_shared_events=expected_shared_events, is_prod=is_prod, - existing_app_flag=existing_app_flag, events_definitions_in_app=events_definitions_in_app, - programming_errno=programming_errno, + error_raised=error_raised, ) else: return _setup_mocks_for_create_app( + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -219,11 +230,12 @@ def _setup_mocks_for_app( expected_shared_events=expected_shared_events, is_prod=is_prod, events_definitions_in_app=events_definitions_in_app, - programming_errno=programming_errno, + error_raised=error_raised, ) def _setup_mocks_for_create_app( + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -231,64 +243,16 @@ def _setup_mocks_for_create_app( expected_shared_events=None, events_definitions_in_app=None, is_prod=False, - programming_errno=None, + error_raised=None, ): mock_get_existing_app_info.return_value = None - authorize_telemetry_clause = "" - if expected_authorize_telemetry_flag is not None: - authorize_telemetry_clause = f" AUTHORIZE_TELEMETRY_EVENT_SHARING = {expected_authorize_telemetry_flag}".upper() - install_clause = "using @app_pkg.app_src.stage debug_mode = True" - if is_prod: - install_clause = " " - calls = [ ( mock_cursor([("old_role",)], []), mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - (ProgrammingError(errno=programming_errno) if programming_errno else None), - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg {install_clause}{authorize_telemetry_clause} - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), ( mock_cursor( events_definitions_in_app or [], ["name", "type", "sharing", "status"] @@ -298,30 +262,82 @@ def _setup_mocks_for_create_app( cursor_class=DictCursor, ), ), + (None, mock.call("use role old_role")), ] if expected_shared_events is not None: - calls.append( - ( - None, - mock.call( - f"""alter application myapp set shared telemetry events ({", ".join([f"'SNOWFLAKE${x}'" for x in expected_shared_events])})""" + calls.extend( + [ + ( + mock_cursor([("old_role",)], []), + mock.call("select current_role()"), ), - ), + (None, mock.call("use role app_role")), + ( + None, + mock.call( + f"""alter application myapp set shared telemetry events ({", ".join([f"'SNOWFLAKE${x}'" for x in expected_shared_events])})""" + ), + ), + (None, mock.call("use role old_role")), + ] ) - calls.extend( - [ - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) side_effects, mock_execute_query_expected = mock_execute_helper(calls) mock_execute_query.side_effect = side_effects - return mock_execute_query_expected + + mock_sql_facade_create_application.side_effect = error_raised or mock_cursor( + [[(DEFAULT_SUCCESS_MESSAGE,)]], [] + ) + + mock_sql_facade_create_application_expected = [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.release_directive() + if is_prod + else SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=None, + should_authorize_event_sharing=expected_authorize_telemetry_flag, + role="app_role", + warehouse="app_warehouse", + ) + ] + + mock_sql_facade_grant_privileges_to_role_expected = [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + return [ + *mock_execute_query_expected, + *mock_sql_facade_create_application_expected, + *mock_sql_facade_grant_privileges_to_role_expected, + ] def _setup_mocks_for_upgrade_app( + mock_sql_facade_upgrade_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -329,15 +345,12 @@ def _setup_mocks_for_upgrade_app( expected_shared_events=None, events_definitions_in_app=None, is_prod=False, - existing_app_flag=False, - programming_errno=None, + error_raised=None, ): - mock_get_existing_app_info.return_value = { + mock_get_existing_app_info_result = { "comment": "GENERATED_BY_SNOWFLAKECLI", } - install_clause = "using @app_pkg.app_src.stage" - if is_prod: - install_clause = "" + mock_get_existing_app_info.return_value = mock_get_existing_app_info_result calls = [ ( @@ -345,16 +358,6 @@ def _setup_mocks_for_upgrade_app( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - (None, mock.call(f"alter application myapp upgrade {install_clause}")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), ( mock_cursor( events_definitions_in_app or [], ["name", "type", "sharing", "status"] @@ -364,70 +367,60 @@ def _setup_mocks_for_upgrade_app( cursor_class=DictCursor, ), ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor( - [ - { - "property": "authorize_telemetry_event_sharing", - "value": str(existing_app_flag).lower(), - } - ], - ["property", "value"], - ), - mock.call( - "desc application myapp", - cursor_class=DictCursor, - ), - ), + (None, mock.call("use role old_role")), ] - if expected_authorize_telemetry_flag is not None: - calls.append( - ( + if expected_shared_events is not None: + calls.extend( + [ ( - ProgrammingError(errno=programming_errno) - if programming_errno - else None + mock_cursor([("old_role",)], []), + mock.call("select current_role()"), ), - mock.call( - f"alter application myapp set AUTHORIZE_TELEMETRY_EVENT_SHARING = {str(expected_authorize_telemetry_flag).upper()}" - ), - ), - ) - - if expected_shared_events is not None: - calls.append( - ( - None, - mock.call( - f"""alter application myapp set shared telemetry events ({", ".join([f"'SNOWFLAKE${x}'" for x in expected_shared_events])})""" + (None, mock.call("use role app_role")), + ( + None, + mock.call( + f"""alter application myapp set shared telemetry events ({", ".join([f"'SNOWFLAKE${x}'" for x in expected_shared_events])})""" + ), ), - ), + (None, mock.call("use role old_role")), + ], ) - calls.extend( - [ - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) side_effects, mock_execute_query_expected = mock_execute_helper(calls) mock_execute_query.side_effect = side_effects - return mock_execute_query_expected + mock_sql_facade_upgrade_application.side_effect = error_raised or mock_cursor( + [[(DEFAULT_SUCCESS_MESSAGE,)]], [] + ) + mock_sql_facade_upgrade_application_expected = [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive() + if is_prod + else SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=None, + should_authorize_event_sharing=expected_authorize_telemetry_flag, + role="app_role", + warehouse="app_warehouse", + ) + ] + return [*mock_execute_query_expected, *mock_sql_facade_upgrade_application_expected] -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -459,6 +452,9 @@ def test_event_sharing_disabled_no_change_to_current_behavior( mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -467,7 +463,9 @@ def test_event_sharing_disabled_no_change_to_current_behavior( temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -487,18 +485,27 @@ def test_event_sharing_disabled_no_change_to_current_behavior( console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -521,6 +528,9 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -529,7 +539,9 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -550,20 +562,32 @@ def test_event_sharing_disabled_but_we_add_event_sharing_flag_in_project_definit console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_called_with( - "WARNING: Same-account event sharing is not enabled in your account, therefore, application telemetry section will be ignored." - ) + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + + assert mock_console.warning.mock_calls == [ + mock.call( + "WARNING: Same-account event sharing is not enabled in your account, therefore, application telemetry section will be ignored." + ), + mock.call(DEFAULT_SUCCESS_MESSAGE), + ] -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -585,6 +609,9 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -593,14 +620,15 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, expected_authorize_telemetry_flag=share_mandatory_events, is_upgrade=is_upgrade, - existing_app_flag=not share_mandatory_events, # existing app with opposite flag to test that flag has changed expected_shared_events=[] if share_mandatory_events else None, ) mock_conn.return_value = MockConnectionCtx() @@ -616,18 +644,27 @@ def test_event_sharing_enabled_not_enforced_no_mandatory_events_then_flag_respec console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "true", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: True, }, ) @pytest.mark.parametrize( @@ -649,6 +686,9 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -657,14 +697,15 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, - expected_authorize_telemetry_flag=None, # make sure flag is not set again during upgrade + expected_authorize_telemetry_flag=share_mandatory_events, is_upgrade=is_upgrade, - existing_app_flag=share_mandatory_events, # existing app with same flag as target app expected_shared_events=[] if share_mandatory_events else None, ) mock_conn.return_value = MockConnectionCtx() @@ -680,18 +721,27 @@ def test_event_sharing_enabled_when_upgrade_flag_matches_existing_app_then_do_no console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -711,6 +761,9 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -719,14 +772,15 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, expected_authorize_telemetry_flag=share_mandatory_events, is_upgrade=is_upgrade, - existing_app_flag=not share_mandatory_events, # existing app with opposite flag to test that flag has changed expected_shared_events=["ERRORS_AND_WARNINGS"], events_definitions_in_app=[ { @@ -750,18 +804,27 @@ def test_event_sharing_enabled_with_mandatory_events_and_explicit_authorization_ console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert expected == [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -781,6 +844,9 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -789,16 +855,15 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, - expected_authorize_telemetry_flag=( - None if is_upgrade else share_mandatory_events - ), + expected_authorize_telemetry_flag=share_mandatory_events, is_upgrade=is_upgrade, - existing_app_flag=False, # we can't switch from True to False, so we assume False expected_shared_events=[] if share_mandatory_events else None, events_definitions_in_app=[ { @@ -822,21 +887,32 @@ def test_event_sharing_enabled_with_mandatory_events_but_no_authorization_then_f console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - - mock_console.warning.assert_called_with( - "WARNING: Mandatory events are present in the application, but event sharing is not authorized in the application telemetry field. This will soon be required to set in order to deploy this application." - ) + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + + assert mock_console.warning.mock_calls == [ + mock.call(DEFAULT_SUCCESS_MESSAGE), + mock.call( + "WARNING: Mandatory events are present in the application, but event sharing is not authorized in the application telemetry field. This will soon be required to set in order to deploy this application." + ), + ] -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "true", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: True, }, ) @pytest.mark.parametrize( @@ -856,6 +932,9 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -864,14 +943,15 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, expected_authorize_telemetry_flag=share_mandatory_events, is_upgrade=is_upgrade, - existing_app_flag=not share_mandatory_events, # existing app with opposite flag to test that flag has changed expected_shared_events=[] if share_mandatory_events else None, ) mock_conn.return_value = MockConnectionCtx() @@ -887,18 +967,27 @@ def test_enforced_events_sharing_with_no_mandatory_events_then_use_value_provide console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "true", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: True, }, ) @pytest.mark.parametrize( @@ -918,6 +1007,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -926,7 +1018,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -948,18 +1042,27 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_provide console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "true", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: True, }, ) @pytest.mark.parametrize( @@ -979,6 +1082,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -988,12 +1094,13 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused mock_cursor, ): mock_execute_query_expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, expected_authorize_telemetry_flag=share_mandatory_events, - existing_app_flag=not share_mandatory_events, # existing app with opposite flag to test that flag has changed is_upgrade=is_upgrade, events_definitions_in_app=[ { @@ -1003,7 +1110,12 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused "status": "ENABLED", } ], - programming_errno=APPLICATION_REQUIRES_TELEMETRY_SHARING, + error_raised=mock_side_effect_error_with_cause( + UserInputError( + "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ), + ProgrammingError(errno=APPLICATION_REQUIRES_TELEMETRY_SHARING), + ), ) mock_conn.return_value = MockConnectionCtx() _setup_project( @@ -1026,14 +1138,17 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_refused mock_console.warning.assert_not_called() -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "true", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: True, }, ) @pytest.mark.parametrize( @@ -1053,6 +1168,9 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1061,13 +1179,14 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, is_prod=not install_method.is_dev_mode, expected_authorize_telemetry_flag=share_mandatory_events, - existing_app_flag=not share_mandatory_events, # existing app with opposite flag to test that flag has changed is_upgrade=is_upgrade, events_definitions_in_app=[ { @@ -1077,7 +1196,12 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio "status": "ENABLED", } ], - programming_errno=CANNOT_DISABLE_MANDATORY_TELEMETRY, + error_raised=mock_side_effect_error_with_cause( + UserInputError( + "Could not disable telemetry event sharing for the application because it contains mandatory events. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ), + ProgrammingError(errno=CANNOT_DISABLE_MANDATORY_TELEMETRY), + ), ) mock_conn.return_value = MockConnectionCtx() _setup_project( @@ -1086,7 +1210,7 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio ) mock_console = MagicMock() - with pytest.raises(ClickException) as e: + with pytest.raises(UserInputError) as e: _create_or_upgrade_app( policy=MagicMock(), install_method=install_method, @@ -1100,14 +1224,17 @@ def test_enforced_events_sharing_with_mandatory_events_manifest_and_authorizatio mock_console.warning.assert_not_called() -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "true", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: True, }, ) @pytest.mark.parametrize( @@ -1126,6 +1253,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1134,7 +1264,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -1156,19 +1288,30 @@ def test_enforced_events_sharing_with_mandatory_events_and_dev_mode_then_default console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected expected_warning = "WARNING: Mandatory events are present in the manifest file. Automatically authorizing event sharing in dev mode. To suppress this warning, please add 'share_mandatory_events: true' in the application telemetry section." - mock_console.warning.assert_called_with(expected_warning) + assert mock_console.warning.mock_calls == [ + mock.call(expected_warning), + mock.call(DEFAULT_SUCCESS_MESSAGE), + ] -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "true", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: True, }, ) @pytest.mark.parametrize( @@ -1187,6 +1330,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1195,7 +1341,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -1210,7 +1358,12 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe "status": "ENABLED", } ], - programming_errno=APPLICATION_REQUIRES_TELEMETRY_SHARING, + error_raised=mock_side_effect_error_with_cause( + UserInputError( + "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ), + ProgrammingError(errno=APPLICATION_REQUIRES_TELEMETRY_SHARING), + ), ) mock_conn.return_value = MockConnectionCtx() _setup_project( @@ -1233,14 +1386,17 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe mock_console.warning.assert_not_called() -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "true", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: True, }, ) @pytest.mark.parametrize( @@ -1259,6 +1415,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1267,7 +1426,9 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -1296,17 +1457,27 @@ def test_enforced_events_sharing_with_mandatory_events_and_authorization_not_spe console=mock_console, ) - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) + +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "true", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: True, }, ) @pytest.mark.parametrize( @@ -1325,6 +1496,9 @@ def test_shared_events_with_no_enabled_mandatory_events_then_error( mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1333,7 +1507,9 @@ def test_shared_events_with_no_enabled_mandatory_events_then_error( temp_dir, mock_cursor, ): - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -1363,14 +1539,17 @@ def test_shared_events_with_no_enabled_mandatory_events_then_error( mock_console.warning.assert_not_called() -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "true", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "true", + UIParameter.NA_EVENT_SHARING_V2: True, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: True, }, ) @pytest.mark.parametrize( @@ -1390,6 +1569,9 @@ def test_shared_events_with_authorization_then_success( mock_param, mock_conn, mock_execute_query, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, manifest_contents, share_mandatory_events, @@ -1399,7 +1581,9 @@ def test_shared_events_with_authorization_then_success( mock_cursor, ): shared_events = ["DEBUG_LOGS", "ERRORS_AND_WARNINGS"] - mock_execute_query_expected = _setup_mocks_for_app( + expected = _setup_mocks_for_app( + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_execute_query, mock_cursor, mock_get_existing_app_info, @@ -1436,5 +1620,11 @@ def test_shared_events_with_authorization_then_success( console=mock_console, ) - assert mock_execute_query.mock_calls == mock_execute_query_expected - mock_console.warning.assert_not_called() + assert [ + *mock_execute_query.mock_calls, + *mock_sql_facade_upgrade_application.mock_calls, + *mock_sql_facade_create_application.mock_calls, + *mock_sql_facade_grant_privileges_to_role.mock_calls, + ] == expected + + mock_console.warning.assert_called_once_with(DEFAULT_SUCCESS_MESSAGE) diff --git a/tests/nativeapp/test_feature_flags.py b/tests/nativeapp/test_feature_flags.py index 921f87c7ba..6c40d47ad4 100644 --- a/tests/nativeapp/test_feature_flags.py +++ b/tests/nativeapp/test_feature_flags.py @@ -27,5 +27,5 @@ def test_feature_setup_script_generation_enabled( assert FeatureFlag.ENABLE_NATIVE_APP_PYTHON_SETUP.is_enabled() is value_from_config mock_get_config_value.assert_called_once_with( - "cli", "features", key="enable_native_app_python_setup", default=False + "cli", "features", key="enable_native_app_python_setup", default=None ) diff --git a/tests/nativeapp/test_manager.py b/tests/nativeapp/test_manager.py index 35bf4f0c10..57cafe7a07 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, @@ -79,9 +80,12 @@ APP_PACKAGE_ENTITY_IS_DISTRIBUTION_SAME, ENTITIES_UTILS_MODULE, SQL_EXECUTOR_EXECUTE, + SQL_FACADE_ALTER_APP_PKG_PROPERTIES, + SQL_FACADE_CREATE_APP_PKG, SQL_FACADE_CREATE_SCHEMA, SQL_FACADE_CREATE_STAGE, SQL_FACADE_GET_ACCOUNT_EVENT_TABLE, + SQL_FACADE_GET_UI_PARAMETER, SQL_FACADE_STAGE_EXISTS, mock_execute_helper, mock_snowflake_yml_file_v2, @@ -521,7 +525,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 +561,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 @@ -772,38 +776,21 @@ def test_get_snowsight_url_without_pdf_warehouse( # Test create_app_package() with no existing package available -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(APP_PACKAGE_ENTITY_GET_EXISTING_APP_PKG_INFO, return_value=None) -def test_create_app_pkg_no_existing_package( +@mock.patch(SQL_FACADE_GET_UI_PARAMETER, return_value="ENABLED") +@mock.patch(SQL_FACADE_CREATE_APP_PKG) +@mock.patch("snowflake.cli.api.config.get_config_value") +@pytest.mark.parametrize("feature_flag", [True, False, None]) +def test_given_no_existing_pkg_when_create_app_pkg_then_success_and_respect_release_channels_flag( + mock_get_config_value, + mock_create_app_pkg, + mock_get_ui_parameter, mock_get_existing_app_pkg_info, - mock_execute, temp_dir, - mock_cursor, workspace_context, + feature_flag, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application package app_pkg - comment = {SPECIAL_COMMENT} - distribution = internal - """ - ) - ), - ), - (None, mock.call("use role old_role")), - ] - ) - mock_execute.side_effect = side_effects + mock_get_config_value.return_value = feature_flag current_working_directory = os.getcwd() create_named_file( @@ -815,9 +802,70 @@ def test_create_app_pkg_no_existing_package( dm = _get_dm() pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities["app_pkg"] pkg = ApplicationPackageEntity(pkg_model, workspace_context) + pkg.create_app_package() - assert mock_execute.mock_calls == expected + mock_get_existing_app_pkg_info.assert_called_once() + mock_create_app_pkg.assert_called_once_with( + package_name="app_pkg", + distribution="internal", + enable_release_channels=feature_flag, + role="package_role", + ) + mock_get_config_value.assert_called_once_with( + "cli", "features", key="enable_release_channels", default=None + ) + + +@mock.patch(APP_PACKAGE_ENTITY_GET_EXISTING_APP_PKG_INFO) +@mock_get_app_pkg_distribution_in_sf() +@mock.patch(APP_PACKAGE_ENTITY_IS_DISTRIBUTION_SAME) +@mock.patch(SQL_FACADE_GET_UI_PARAMETER, return_value="ENABLED") +@mock.patch(SQL_FACADE_ALTER_APP_PKG_PROPERTIES) +@mock.patch("snowflake.cli.api.config.get_config_value") +@pytest.mark.parametrize("feature_flag", [True, False, None]) +def test_given_existing_app_package_with_feature_flag_set_when_create_pkg_then_set_pkg_property_to_same_value( + mock_get_config_value, + mock_alter_app_pkg_properties, + mock_get_ui_parameter, + mock_is_distribution_same, + mock_get_distribution, + mock_get_existing_app_pkg_info, + temp_dir, + workspace_context, + feature_flag, +): + mock_get_config_value.return_value = feature_flag + mock_is_distribution_same.return_value = True + mock_get_distribution.return_value = "internal" + mock_get_existing_app_pkg_info.return_value = { + "name": "APP_PKG", + "comment": SPECIAL_COMMENT, + "version": LOOSE_FILES_MAGIC_VERSION, + "owner": "PACKAGE_ROLE", + } + + current_working_directory = os.getcwd() + create_named_file( + file_name="snowflake.yml", + dir_name=current_working_directory, + contents=[mock_snowflake_yml_file_v2], + ) + dm = _get_dm() + pkg_model: ApplicationPackageEntityModel = dm.project_definition.entities["app_pkg"] + pkg = ApplicationPackageEntity(pkg_model, workspace_context) + workspace_context.console = mock.MagicMock() + + pkg.create_app_package() + + mock_alter_app_pkg_properties.assert_called_once_with( + package_name="app_pkg", + enable_release_channels=feature_flag, + role="package_role", + ) + mock_get_config_value.assert_called_once_with( + "cli", "features", key="enable_release_channels", default=None + ) # Test create_app_package() with a different owner @@ -825,7 +873,9 @@ def test_create_app_pkg_no_existing_package( @mock.patch(APP_PACKAGE_ENTITY_GET_EXISTING_APP_PKG_INFO) @mock_get_app_pkg_distribution_in_sf() @mock.patch(APP_PACKAGE_ENTITY_IS_DISTRIBUTION_SAME, return_value=True) +@mock.patch(SQL_FACADE_GET_UI_PARAMETER, return_value="ENABLED") def test_create_app_pkg_different_owner( + mock_get_ui_parameter, mock_is_distribution_same, mock_get_distribution, mock_get_existing_app_pkg_info, @@ -868,7 +918,9 @@ def test_create_app_pkg_different_owner( "is_pkg_distribution_same", [False, True], ) +@mock.patch(SQL_FACADE_GET_UI_PARAMETER, return_value="ENABLED") def test_create_app_pkg_external_distribution( + mock_get_ui_parameter, mock_is_distribution_same, mock_get_distribution, mock_get_existing_app_pkg_info, @@ -916,7 +968,9 @@ def test_create_app_pkg_external_distribution( (True, SPECIAL_COMMENT_OLD), ], ) +@mock.patch(SQL_FACADE_GET_UI_PARAMETER, return_value="ENABLED") def test_create_app_pkg_internal_distribution_special_comment( + mock_get_ui_parameter, mock_is_distribution_same, mock_get_distribution, mock_get_existing_app_pkg_info, diff --git a/tests/nativeapp/test_run_processor.py b/tests/nativeapp/test_run_processor.py index 9a91c9f88b..8d2bb4c545 100644 --- a/tests/nativeapp/test_run_processor.py +++ b/tests/nativeapp/test_run_processor.py @@ -22,6 +22,7 @@ from click import UsageError from snowflake.cli._plugins.connection.util import UIParameter from snowflake.cli._plugins.nativeapp.constants import ( + COMMENT_COL, LOOSE_FILES_MAGIC_VERSION, SPECIAL_COMMENT, ) @@ -46,6 +47,12 @@ from snowflake.cli._plugins.nativeapp.same_account_install_method import ( SameAccountInstallMethod, ) +from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType +from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import ( + CouldNotUseObjectError, + UpgradeApplicationRestrictionError, + UserInputError, +) from snowflake.cli._plugins.stage.diff import DiffResult from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext from snowflake.cli._plugins.workspace.manager import WorkspaceManager @@ -57,11 +64,10 @@ APPLICATION_NO_LONGER_AVAILABLE, APPLICATION_OWNS_EXTERNAL_OBJECTS, CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, + CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES, + DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, -) -from snowflake.cli.api.exceptions import ( - CouldNotUseObjectError, - NoWarehouseSelectedInSessionError, + NO_WAREHOUSE_SELECTED_IN_SESSION, ) from snowflake.cli.api.project.definition_manager import DefinitionManager from snowflake.connector import ProgrammingError @@ -71,12 +77,17 @@ mock_connection, ) from tests.nativeapp.utils import ( - APP_ENTITY_GET_EXISTING_APP_INFO, APP_PACKAGE_ENTITY_GET_EXISTING_VERSION_INFO, GET_UI_PARAMETERS, SQL_EXECUTOR_EXECUTE, + SQL_FACADE_CREATE_APPLICATION, + SQL_FACADE_GET_EVENT_DEFINITIONS, + SQL_FACADE_GET_EXISTING_APP_INFO, + SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE, + SQL_FACADE_UPGRADE_APPLICATION, TYPER_CONFIRM, mock_execute_helper, + mock_side_effect_error_with_cause, quoted_override_yml_file_v2, ) from tests.testing_utils.files_and_dirs import create_named_file @@ -113,12 +124,27 @@ def _get_wm(): ) +DEFAULT_APP_ID = "myapp" +DEFAULT_PKG_ID = "app_pkg" +DEFAULT_STAGE_FQN = "app_pkg.app_src.stage" +DEFAULT_UPGRADE_SUCCESS_MESSAGE = "Application successfully upgraded." +DEFAULT_CREATE_SUCCESS_MESSAGE = f"Application '{DEFAULT_APP_ID}' created successfully." +DEFAULT_USER_INPUT_ERROR_MESSAGE = "User input error message." +DEFAULT_GET_EXISTING_APP_INFO_RESULT = { + "name": "myapp", + "comment": SPECIAL_COMMENT, + "owner": "app_role", +} +DEFAULT_ROLE = "app_role" +DEFAULT_WAREHOUSE = "app_warehouse" + + def _create_or_upgrade_app( policy: PolicyBase, install_method: SameAccountInstallMethod, interactive: bool = False, - package_id: str = "app_pkg", - app_id: str = "myapp", + package_id: str = DEFAULT_PKG_ID, + app_id: str = DEFAULT_APP_ID, console: AbstractConsole | None = None, ): dm = DefinitionManager() @@ -199,44 +225,30 @@ def setup_project_file(current_working_directory: str, pdf=None): # Test create_dev_app with exception thrown trying to use the warehouse -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_w_warehouse_access_exception( - mock_param, mock_conn, mock_execute, temp_dir, mock_cursor + mock_param, + mock_conn, + mock_sql_facade_grant_privileges_to_role, + mock_get_existing_app_info, + mock_sql_facade_create_application, + temp_dir, + mock_cursor, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - ( - CouldNotUseObjectError( - object_type=ObjectType.WAREHOUSE, name="app_warehouse" - ), - mock.call("use warehouse app_warehouse"), - ), - ( - None, - mock.call("use warehouse old_wh"), - ), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.side_effect = CouldNotUseObjectError( + object_type=UseObjectType.WAREHOUSE, name="app_warehouse" + ) mock_diff_result = DiffResult() setup_project_file(os.getcwd()) @@ -249,73 +261,70 @@ def test_create_dev_app_w_warehouse_access_exception( install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected assert ( "Could not use warehouse app_warehouse. Object does not exist, or operation cannot be performed." in err.value.message ) + mock_sql_facade_create_application.assert_called_once_with( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] # Test create_dev_app with no existing application AND create succeeds AND app role == package role -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_create_new_w_no_additional_privileges( mock_param, mock_conn, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult() @@ -326,18 +335,34 @@ def test_create_dev_app_create_new_w_no_additional_privileges( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test create_dev_app with no existing application AND create returns a warning -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -355,97 +380,21 @@ def test_create_dev_app_create_new_w_no_additional_privileges( def test_create_or_upgrade_dev_app_with_warning( mock_param, mock_conn, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, existing_app_info, ): status_messages = ["App created/upgraded", "Warning: some warning"] - status_cursor = mock_cursor( - [(msg,) for msg in status_messages], - ["status"], - ) - create_or_upgrade_calls = ( - [ - ( - status_cursor, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - ] - if existing_app_info is None - else [ - ( - status_cursor, - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "desc application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("alter application myapp set debug_mode = True")), - ] - ) + status_cursor_results = [(msg,) for msg in status_messages] mock_get_existing_app_info.return_value = existing_app_info - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - *create_or_upgrade_calls, - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.return_value = status_cursor_results + mock_sql_facade_upgrade_application.return_value = status_cursor_results mock_diff_result = DiffResult() setup_project_file(os.getcwd(), test_pdf.replace("package_role", "app_role")) @@ -457,91 +406,68 @@ def test_create_or_upgrade_dev_app_with_warning( install_method=SameAccountInstallMethod.unversioned_dev(), console=mock_console, ) - assert mock_execute.mock_calls == expected + if existing_app_info is None: + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + mock_sql_facade_upgrade_application.assert_not_called() + else: + mock_sql_facade_create_application.assert_not_called() + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) mock_console.warning.assert_has_calls([mock.call(msg) for msg in status_messages]) # Test create_dev_app with no existing application AND create succeeds AND app role != package role -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_create_new_with_additional_privileges( mock_param, mock_conn, - mock_execute_query, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, ): - side_effects, mock_execute_query_expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute_query.side_effect = side_effects + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult() setup_project_file(os.getcwd()) @@ -550,87 +476,107 @@ def test_create_dev_app_create_new_with_additional_privileges( _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) - assert mock_execute_query.mock_calls == mock_execute_query_expected + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test create_dev_app with no existing application AND create throws an exception -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_create_new_w_missing_warehouse_exception( mock_param, mock_conn, - mock_execute, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - NoWarehouseSelectedInSessionError( - msg="No active warehouse selected in the current session" - ), - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) - mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.side_effect = mock_side_effect_error_with_cause( + err=UserInputError("No active warehouse selected in the current session"), + cause=ProgrammingError(errno=NO_WAREHOUSE_SELECTED_IN_SESSION), + ) mock_diff_result = DiffResult() setup_project_file(os.getcwd(), test_pdf.replace("package_role", "app_role")) assert not mock_diff_result.has_changes() - with pytest.raises(NoWarehouseSelectedInSessionError) as err: + with pytest.raises(UserInputError) as err: _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert "Please provide a warehouse for the active session role" in err.value.message - assert mock_execute.mock_calls == expected + assert err.match("No active warehouse selected in the current session") + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] # Test create_dev_app with existing application AND bad comment AND good version # Test create_dev_app with existing application AND bad comment AND bad version -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(SQL_EXECUTOR_EXECUTE) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -656,24 +602,7 @@ def test_create_dev_app_incorrect_properties( "version": version, "owner": "APP_ROLE", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects mock_diff_result = DiffResult() setup_project_file(os.getcwd()) @@ -685,24 +614,27 @@ def test_create_dev_app_incorrect_properties( install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected + assert mock_get_existing_app_info.mock_calls == [ + mock.call(DEFAULT_APP_ID, DEFAULT_ROLE), + mock.call(DEFAULT_APP_ID, DEFAULT_ROLE), + ] # Test create_dev_app with existing application AND incorrect owner -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_incorrect_owner( mock_param, mock_conn, - mock_execute, + mock_sql_facade_upgrade_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -713,62 +645,56 @@ def test_create_dev_app_incorrect_owner( "version": LOOSE_FILES_MAGIC_VERSION, "owner": "wrong_owner", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - msg="Insufficient privileges to operate on database", - errno=INSUFFICIENT_PRIVILEGES, - ), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) + mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UserInputError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError( + msg="Insufficient privileges to operate on database", + errno=INSUFFICIENT_PRIVILEGES, + ), + ) mock_diff_result = DiffResult() setup_project_file(os.getcwd()) - with pytest.raises(ProgrammingError): + with pytest.raises(UserInputError): assert not mock_diff_result.has_changes() _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] # Test create_dev_app with existing application AND diff has no changes -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @mock_connection() def test_create_dev_app_no_diff_changes( mock_param, mock_conn, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_upgrade_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -779,53 +705,11 @@ def test_create_dev_app_no_diff_changes( "version": LOOSE_FILES_MAGIC_VERSION, "owner": "APP_ROLE", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - None, - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "desc application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("alter application myapp set debug_mode = True")), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) + mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_cursor( + [[(DEFAULT_UPGRADE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult() setup_project_file(os.getcwd()) @@ -834,24 +718,39 @@ def test_create_dev_app_no_diff_changes( _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test create_dev_app with existing application AND diff has changes -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_w_diff_changes( mock_param, mock_conn, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_upgrade_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -862,53 +761,11 @@ def test_create_dev_app_w_diff_changes( "version": LOOSE_FILES_MAGIC_VERSION, "owner": "APP_ROLE", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - None, - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "desc application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("alter application myapp set debug_mode = True")), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) + mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_cursor( + [[(DEFAULT_UPGRADE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult(different=["setup.sql"]) setup_project_file(os.getcwd()) @@ -917,24 +774,37 @@ def test_create_dev_app_w_diff_changes( _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test create_dev_app with existing application AND alter throws an error -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_recreate_w_missing_warehouse_exception( mock_param, mock_conn, - mock_execute, + mock_sql_facade_upgrade_application, mock_get_existing_app_info, temp_dir, mock_cursor, @@ -945,108 +815,51 @@ def test_create_dev_app_recreate_w_missing_warehouse_exception( "version": LOOSE_FILES_MAGIC_VERSION, "owner": "APP_ROLE", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - NoWarehouseSelectedInSessionError( - msg="No active warehouse selected in the current session" - ), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UserInputError("No active warehouse selected in the current session"), + cause=ProgrammingError(errno=NO_WAREHOUSE_SELECTED_IN_SESSION), + ) mock_diff_result = DiffResult(different=["setup.sql"]) setup_project_file(os.getcwd()) assert mock_diff_result.has_changes() - with pytest.raises(NoWarehouseSelectedInSessionError) as err: + with pytest.raises(UserInputError) as err: _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev(), ) - assert mock_execute.mock_calls == expected - assert "Please provide a warehouse for the active session role" in err.value.message + assert err.match("No active warehouse selected in the current session") # Test create_dev_app with no existing application AND quoted name scenario 1 -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_create_new_quoted( mock_param, mock_conn, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_create_application, mock_get_existing_app_info, temp_dir, mock_cursor, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - None, - mock.call( - dedent( - f"""\ - create application "My Application" - from application package "My Package" using '@"My Package".app_src.stage' debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - 'show telemetry event definitions in application "My Application"', - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult() pdf_content = dedent( @@ -1084,69 +897,48 @@ def test_create_dev_app_create_new_quoted( _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name='"My Application"', + package_name='"My Package"', + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn='"My Package".app_src.stage', + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + mock_sql_facade_get_event_definitions.assert_called_once_with( + '"My Application"', DEFAULT_ROLE + ) # Test create_dev_app with no existing application AND quoted name scenario 2 -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) -@mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS, return_value=[]) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_create_new_quoted_override( mock_param, mock_conn, - mock_execute, + mock_sql_facade_create_application, + mock_sql_facade_get_event_definitions, mock_get_existing_app_info, temp_dir, mock_cursor, ): - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - None, - mock.call( - dedent( - f"""\ - create application "My Application" - from application package "My Package" using '@"My Package".app_src.stage' debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - 'show telemetry event definitions in application "My Application"', - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) mock_diff_result = DiffResult() current_working_directory = os.getcwd() @@ -1163,7 +955,19 @@ def test_create_dev_app_create_new_quoted_override( _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) - assert mock_execute.mock_calls == expected + mock_sql_facade_create_application.assert_called_once_with( + name='"My Application"', + package_name='"My Package"', + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn='"My Package".app_src.stage', + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + mock_sql_facade_get_event_definitions.assert_called_once_with( + '"My Application"', DEFAULT_ROLE + ) # Test run existing app info @@ -1171,14 +975,18 @@ def test_create_dev_app_create_new_quoted_override( # AND user wants to drop app # AND drop succeeds # AND app is created successfully. +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_recreate_app_when_orphaned( @@ -1186,6 +994,10 @@ def test_create_dev_app_recreate_app_when_orphaned( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1195,6 +1007,7 @@ def test_create_dev_app_recreate_app_when_orphaned( "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } + side_effects, expected = mock_execute_helper( [ ( @@ -1202,74 +1015,78 @@ def test_create_dev_app_recreate_app_when_orphaned( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), (None, mock.call("drop application myapp")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), + ) + + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) + setup_project_file(os.getcwd()) _create_or_upgrade_app( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test run existing app info @@ -1278,14 +1095,18 @@ def test_create_dev_app_recreate_app_when_orphaned( # AND drop requires cascade # AND drop succeeds # AND app is created successfully. +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( @@ -1293,6 +1114,10 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1302,7 +1127,7 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } - # side_effects, expected = mock_execute_helper( + side_effects, expected = mock_execute_helper( [ ( @@ -1310,17 +1135,6 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), ( ProgrammingError(errno=APPLICATION_OWNS_EXTERNAL_OBJECTS), mock.call("drop application myapp"), @@ -1329,6 +1143,10 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( mock_cursor([("app_role",)], []), mock.call("select current_role()"), ), + ( + mock_cursor([("app_role",)], []), + mock.call("select current_role()"), + ), ( mock_cursor( [ @@ -1339,55 +1157,18 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( mock.call("show objects owned by application myapp"), ), (None, mock.call("drop application myapp cascade")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -1395,6 +1176,58 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test run existing app info @@ -1404,14 +1237,18 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade( # AND we can't see which objects are owned by the app # AND drop succeeds # AND app is created successfully. +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_objects( @@ -1419,6 +1256,10 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1428,6 +1269,7 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje "owner": "app_role", "version": LOOSE_FILES_MAGIC_VERSION, } + side_effects, expected = mock_execute_helper( [ ( @@ -1435,17 +1277,6 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), ( ProgrammingError(errno=APPLICATION_OWNS_EXTERNAL_OBJECTS), mock.call("drop application myapp"), @@ -1454,60 +1285,27 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje mock_cursor([("app_role",)], []), mock.call("select current_role()"), ), - ( - ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), - mock.call("show objects owned by application myapp"), - ), - (None, mock.call("drop application myapp cascade")), ( mock_cursor([("app_role",)], []), mock.call("select current_role()"), ), - (None, mock.call("use role package_role")), ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), + ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), + mock.call("show objects owned by application myapp"), ), - (None, mock.call("use warehouse old_wh")), + (None, mock.call("drop application myapp cascade")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=APPLICATION_NO_LONGER_AVAILABLE), + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -1515,23 +1313,82 @@ def test_create_dev_app_recreate_app_when_orphaned_requires_cascade_unknown_obje policy=MagicMock(), install_method=SameAccountInstallMethod.unversioned_dev() ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test upgrade app method for release directives AND throws warehouse error @mock.patch(SQL_EXECUTOR_EXECUTE) +@mock.patch( + SQL_FACADE_GET_EXISTING_APP_INFO, return_value={COMMENT_COL: SPECIAL_COMMENT} +) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( "policy_param", [allow_always_policy, ask_always_policy, deny_always_policy] ) def test_upgrade_app_warehouse_error( - mock_param, mock_conn, mock_execute, policy_param, temp_dir, mock_cursor + mock_param, + mock_conn, + mock_get_existing_app_info, + mock_execute, + policy_param, + temp_dir, + mock_cursor, ): side_effects, expected = mock_execute_helper( [ @@ -1542,17 +1399,11 @@ def test_upgrade_app_warehouse_error( (None, mock.call("use role app_role")), ( mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - ( - CouldNotUseObjectError( - object_type=ObjectType.WAREHOUSE, name="app_warehouse" - ), - mock.call("use warehouse app_warehouse"), + mock.call("select current_warehouse()"), ), ( - None, - mock.call("use warehouse old_wh"), + ProgrammingError(errno=DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED), + mock.call("use warehouse app_warehouse"), ), (None, mock.call("use role old_role")), ] @@ -1572,14 +1423,15 @@ 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, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -1590,6 +1442,7 @@ def test_upgrade_app_incorrect_owner( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, policy_param, temp_dir, mock_cursor, @@ -1599,52 +1452,47 @@ def test_upgrade_app_incorrect_owner( "comment": SPECIAL_COMMENT, "owner": "wrong_owner", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - msg="Insufficient privileges to operate on database", - errno=INSUFFICIENT_PRIVILEGES, - ), - mock.call("alter application myapp upgrade "), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) + mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UserInputError("Insufficient privileges to operate on database"), + cause=ProgrammingError( + errno=INSUFFICIENT_PRIVILEGES, msg="Some error message." + ), + ) setup_project_file(os.getcwd()) - with pytest.raises(ProgrammingError): + with pytest.raises(UserInputError) as err: _create_or_upgrade_app( policy=policy_param, interactive=True, install_method=SameAccountInstallMethod.release_directive(), ) - assert mock_execute.mock_calls == expected + err.match("Insufficient privileges to operate on database") + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] # Test upgrade app method for release directives AND existing app info AND upgrade succeeds -@mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -1654,7 +1502,8 @@ def test_upgrade_app_succeeds( mock_param, mock_conn, mock_get_existing_app_info, - mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_upgrade_application, policy_param, temp_dir, mock_cursor, @@ -1664,47 +1513,11 @@ def test_upgrade_app_succeeds( "comment": SPECIAL_COMMENT, "owner": "app_role", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - (None, mock.call("alter application myapp upgrade ")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "desc application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) + mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_cursor( + [[(DEFAULT_UPGRADE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -1713,18 +1526,29 @@ def test_upgrade_app_succeeds( interactive=True, install_method=SameAccountInstallMethod.release_directive(), ) - assert mock_execute.mock_calls == expected + mock_sql_facade_upgrade_application.assert_called_once_with( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test upgrade app method for release directives AND existing app info AND upgrade fails due to generic error -@mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -1734,7 +1558,7 @@ def test_upgrade_app_fails_generic_error( mock_param, mock_conn, mock_get_existing_app_info, - mock_execute, + mock_sql_facade_upgrade_application, policy_param, temp_dir, mock_cursor, @@ -1744,47 +1568,41 @@ def test_upgrade_app_fails_generic_error( "comment": SPECIAL_COMMENT, "owner": "app_role", } - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - errno=1234, - ), - mock.call("alter application myapp upgrade "), - ), - (None, mock.call("use warehouse old_wh")), - (None, mock.call("use role old_role")), - ] - ) mock_conn.return_value = MockConnectionCtx() - mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UserInputError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError( + errno=1234, + ), + ) setup_project_file(os.getcwd()) - with pytest.raises(ProgrammingError): + with pytest.raises(UserInputError): _create_or_upgrade_app( policy=policy_param, interactive=True, install_method=SameAccountInstallMethod.release_directive(), ) - assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is False AND interactive mode is False AND --interactive is False # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is False AND interactive mode is False AND --interactive is True AND user does not want to proceed # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is False AND interactive mode is True AND user does not want to proceed +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=False ) @@ -1792,8 +1610,8 @@ def test_upgrade_app_fails_generic_error( @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -1804,8 +1622,9 @@ def test_upgrade_app_fails_upgrade_restriction_error( mock_param, mock_conn, mock_typer_confirm, - mock_get_existing_app_info, mock_execute, + mock_get_existing_app_info, + mock_sql_facade_upgrade_application, policy_param, interactive, expected_code, @@ -1817,6 +1636,13 @@ def test_upgrade_app_fails_upgrade_restriction_error( "comment": SPECIAL_COMMENT, "owner": "app_role", } + + mock_conn.return_value = MockConnectionCtx() + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION), + ) + side_effects, expected = mock_execute_helper( [ ( @@ -1824,22 +1650,9 @@ def test_upgrade_app_fails_upgrade_restriction_error( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - ), - mock.call("alter application myapp upgrade "), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) - mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects setup_project_file(os.getcwd()) @@ -1851,17 +1664,33 @@ def test_upgrade_app_fails_upgrade_restriction_error( install_method=SameAccountInstallMethod.release_directive(), ) assert result.exit_code == expected_code + + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] assert mock_execute.mock_calls == expected +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS, return_value=[]) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock_connection() @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) def test_versioned_app_upgrade_to_unversioned( @@ -1869,6 +1698,10 @@ def test_versioned_app_upgrade_to_unversioned( mock_conn, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, temp_dir, mock_cursor, ): @@ -1882,6 +1715,7 @@ def test_versioned_app_upgrade_to_unversioned( "owner": "app_role", "version": "v1", } + side_effects, expected = mock_execute_helper( [ ( @@ -1889,70 +1723,19 @@ def test_versioned_app_upgrade_to_unversioned( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - msg="Some Error Message.", - errno=93045, - ), - mock.call( - "alter application myapp upgrade using @app_pkg.app_src.stage" - ), - ), (None, mock.call("drop application myapp")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using @app_pkg.app_src.stage debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=CANNOT_UPGRADE_FROM_VERSION_TO_LOOSE_FILES), + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -1963,12 +1746,60 @@ def test_versioned_app_upgrade_to_unversioned( ) assert mock_execute.mock_calls == expected + mock_sql_facade_upgrade_application.assert_called_once_with( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + + mock_sql_facade_create_application.assert_called_with( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with("myapp", DEFAULT_ROLE) + # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is True AND drop fails # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is False AND interactive mode is False AND --interactive is True AND user wants to proceed AND drop fails # Test upgrade app method for release directives AND existing app info AND upgrade fails due to upgrade restriction error AND --force is False AND interactive mode is True AND user wants to proceed AND drop fails +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=True ) @@ -1976,8 +1807,8 @@ def test_versioned_app_upgrade_to_unversioned( @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize( @@ -1990,6 +1821,7 @@ def test_upgrade_app_fails_drop_fails( mock_typer_confirm, mock_get_existing_app_info, mock_execute, + mock_sql_facade_upgrade_application, policy_param, interactive, temp_dir, @@ -2000,6 +1832,7 @@ def test_upgrade_app_fails_drop_fails( "comment": SPECIAL_COMMENT, "owner": "app_role", } + side_effects, expected = mock_execute_helper( [ ( @@ -2007,30 +1840,21 @@ def test_upgrade_app_fails_drop_fails( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - ), - mock.call("alter application myapp upgrade "), - ), ( ProgrammingError( errno=1234, ), mock.call("drop application myapp"), ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects - + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION), + ) setup_project_file(os.getcwd()) with pytest.raises(ProgrammingError): @@ -2040,11 +1864,26 @@ def test_upgrade_app_fails_drop_fails( install_method=SameAccountInstallMethod.release_directive(), ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] # Test upgrade app method for release directives AND existing app info AND user wants to drop app AND drop succeeds AND app is created successfully. +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=True ) @@ -2052,8 +1891,8 @@ def test_upgrade_app_fails_drop_fails( @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize("policy_param", [allow_always_policy, ask_always_policy]) @@ -2063,6 +1902,10 @@ def test_upgrade_app_recreate_app( mock_typer_confirm, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, policy_param, temp_dir, mock_cursor, @@ -2072,6 +1915,7 @@ def test_upgrade_app_recreate_app( "comment": SPECIAL_COMMENT, "owner": "app_role", } + side_effects, expected = mock_execute_helper( [ ( @@ -2079,67 +1923,19 @@ def test_upgrade_app_recreate_app( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - ), - mock.call("alter application myapp upgrade "), - ), (None, mock.call("drop application myapp")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = mock_side_effect_error_with_cause( + err=UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE), + cause=ProgrammingError(errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION), + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -2149,6 +1945,56 @@ def test_upgrade_app_recreate_app( install_method=SameAccountInstallMethod.release_directive(), ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test upgrade app method for version AND no existing version info @@ -2203,8 +2049,12 @@ def test_upgrade_app_from_version_throws_usage_error_two( APP_PACKAGE_ENTITY_GET_EXISTING_VERSION_INFO, return_value={"key": "val"}, ) +@mock.patch(SQL_FACADE_CREATE_APPLICATION) +@mock.patch(SQL_FACADE_UPGRADE_APPLICATION) +@mock.patch(SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE) +@mock.patch(SQL_FACADE_GET_EVENT_DEFINITIONS) @mock.patch(SQL_EXECUTOR_EXECUTE) -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch( f"snowflake.cli._plugins.nativeapp.policy.{TYPER_CONFIRM}", return_value=True ) @@ -2212,8 +2062,8 @@ def test_upgrade_app_from_version_throws_usage_error_two( @mock.patch( GET_UI_PARAMETERS, return_value={ - UIParameter.NA_EVENT_SHARING_V2: "false", - UIParameter.NA_ENFORCE_MANDATORY_FILTERS: "false", + UIParameter.NA_EVENT_SHARING_V2: False, + UIParameter.NA_ENFORCE_MANDATORY_FILTERS: False, }, ) @pytest.mark.parametrize("policy_param", [allow_always_policy, ask_always_policy]) @@ -2223,6 +2073,10 @@ def test_upgrade_app_recreate_app_from_version( mock_typer_confirm, mock_get_existing_app_info, mock_execute, + mock_sql_facade_get_event_definitions, + mock_sql_facade_grant_privileges_to_role, + mock_sql_facade_upgrade_application, + mock_sql_facade_create_application, mock_existing, policy_param, temp_dir, @@ -2241,67 +2095,18 @@ def test_upgrade_app_recreate_app_from_version( mock.call("select current_role()"), ), (None, mock.call("use role app_role")), - ( - mock_cursor([("old_wh",)], []), - mock.call("select current_warehouse()"), - ), - (None, mock.call("use warehouse app_warehouse")), - ( - ProgrammingError( - errno=CANNOT_UPGRADE_FROM_LOOSE_FILES_TO_VERSION, - ), - mock.call("alter application myapp upgrade using version v1 "), - ), (None, mock.call("drop application myapp")), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call("use role package_role")), - ( - None, - mock.call( - "grant install, develop on application package app_pkg to role app_role" - ), - ), - ( - None, - mock.call("grant usage on schema app_pkg.app_src to role app_role"), - ), - ( - None, - mock.call("grant read on stage app_pkg.app_src.stage to role app_role"), - ), - (None, mock.call("use role app_role")), - ( - None, - mock.call( - dedent( - f"""\ - create application myapp - from application package app_pkg using version v1 debug_mode = True - comment = {SPECIAL_COMMENT} - """ - ) - ), - ), - ( - mock_cursor([("app_role",)], []), - mock.call("select current_role()"), - ), - ( - mock_cursor([], []), - mock.call( - "show telemetry event definitions in application myapp", - cursor_class=DictCursor, - ), - ), - (None, mock.call("use warehouse old_wh")), (None, mock.call("use role old_role")), ] ) mock_conn.return_value = MockConnectionCtx() mock_execute.side_effect = side_effects + mock_sql_facade_upgrade_application.side_effect = ( + UpgradeApplicationRestrictionError(DEFAULT_USER_INPUT_ERROR_MESSAGE) + ) + mock_sql_facade_create_application.side_effect = mock_cursor( + [[(DEFAULT_CREATE_SUCCESS_MESSAGE,)]], [] + ) setup_project_file(os.getcwd()) @@ -2321,6 +2126,57 @@ def test_upgrade_app_recreate_app_from_version( version="v1", ) assert mock_execute.mock_calls == expected + assert mock_sql_facade_upgrade_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + install_method=SameAccountInstallMethod.versioned_dev("v1"), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + assert mock_sql_facade_create_application.mock_calls == [ + mock.call( + name=DEFAULT_APP_ID, + package_name=DEFAULT_PKG_ID, + install_method=SameAccountInstallMethod.versioned_dev("v1"), + stage_fqn=DEFAULT_STAGE_FQN, + debug_mode=True, + should_authorize_event_sharing=None, + role=DEFAULT_ROLE, + warehouse=DEFAULT_WAREHOUSE, + ) + ] + + assert mock_sql_facade_grant_privileges_to_role.mock_calls == [ + mock.call( + privileges=["install", "develop"], + object_type=ObjectType.APPLICATION_PACKAGE, + object_identifier="app_pkg", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["usage"], + object_type=ObjectType.SCHEMA, + object_identifier="app_pkg.app_src", + role_to_grant="app_role", + role_to_use="package_role", + ), + mock.call( + privileges=["read"], + object_type=ObjectType.STAGE, + object_identifier="app_pkg.app_src.stage", + role_to_grant="app_role", + role_to_use="package_role", + ), + ] + + mock_sql_facade_get_event_definitions.assert_called_once_with( + DEFAULT_APP_ID, DEFAULT_ROLE + ) # Test get_existing_version_info returns version info correctly diff --git a/tests/nativeapp/test_sf_sql_facade.py b/tests/nativeapp/test_sf_sql_facade.py index ec5c23cb50..0e847108e5 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -17,6 +17,16 @@ from unittest.mock import _Call as Call import pytest +from snowflake.cli._plugins.connection.util import UIParameter +from snowflake.cli._plugins.nativeapp.constants import ( + AUTHORIZE_TELEMETRY_COL, + COMMENT_COL, + NAME_COL, + SPECIAL_COMMENT, +) +from snowflake.cli._plugins.nativeapp.same_account_install_method import ( + SameAccountInstallMethod, +) from snowflake.cli._plugins.nativeapp.sf_facade_constants import UseObjectType from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import ( CouldNotUseObjectError, @@ -24,15 +34,21 @@ InvalidSQLError, UnknownConnectorError, UnknownSQLError, + UserInputError, UserScriptError, ) from snowflake.cli._plugins.nativeapp.sf_sql_facade import ( SnowflakeSQLFacade, ) +from snowflake.cli.api.constants import ObjectType from snowflake.cli.api.errno import ( + APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + APPLICATION_REQUIRES_TELEMETRY_SHARING, + CANNOT_DISABLE_MANDATORY_TELEMETRY, DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, + SQL_COMPILATION_ERROR, ) from snowflake.connector import DatabaseError, DictCursor, Error from snowflake.connector.errors import ( @@ -44,10 +60,11 @@ from tests.nativeapp.utils import ( SQL_EXECUTOR_EXECUTE, SQL_EXECUTOR_EXECUTE_QUERIES, + assert_programmingerror_cause_with_errno, mock_execute_helper, ) -sql_facade = None +sql_facade = SnowflakeSQLFacade() @pytest.fixture(autouse=True) @@ -86,6 +103,22 @@ def mock_use_schema(): yield mock_use_schema +@pytest.fixture +def mock_get_app_properties(): + with mock.patch.object(sql_facade, "get_app_properties") as mock_get_app_properties: + mock_get_app_properties.return_value = {AUTHORIZE_TELEMETRY_COL: "false"} + yield mock_get_app_properties + + +@pytest.fixture +def mock_get_existing_app_info(): + with mock.patch.object( + sql_facade, "get_existing_app_info" + ) as mock_get_existing_app_info: + mock_get_existing_app_info.return_value = {COMMENT_COL: SPECIAL_COMMENT} + yield mock_get_existing_app_info + + @contextmanager def assert_in_context( mock_cms: list[tuple[mock.Mock, Call]], @@ -137,7 +170,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 @@ -1242,16 +1275,6 @@ def test_get_app_properties_bubbles_errors(mock_execute_query): assert f"Failed to describe application {app_name}. {error_message}" in str(err) -expected_ui_params_query = dedent( - f""" - select value['value']::string as PARAM_VALUE, value['name']::string as PARAM_NAME from table(flatten( - input => parse_json(SYSTEM$BOOTSTRAP_DATA_REQUEST()), - path => 'clientParamsInfo' - )) where value['name'] in ('ENABLE_EVENT_SHARING_V2_IN_THE_SAME_ACCOUNT', 'ENFORCE_MANDATORY_FILTERS_FOR_SAME_ACCOUNT_INSTALLATION', 'UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT'); - """ -) - - @mock.patch(SQL_EXECUTOR_EXECUTE) @pytest.mark.parametrize( "events, expected_result", @@ -1637,3 +1660,990 @@ def test_create_stage_raises_insufficient_privileges_error( sql_facade.create_stage(stage, role=role, database=database) mock_execute_query.assert_has_calls(expected) + + +@pytest.mark.parametrize( + "args,expected_query", + [ + ( + { + "privileges": ["install", "develop"], + "object_type": ObjectType.APPLICATION_PACKAGE, + "object_identifier": "package_name", + "role_to_grant": "app_role", + "role_to_use": "package_role", + }, + "grant install, develop on application package package_name to role app_role", + ), + ( + { + "privileges": ["usage"], + "object_type": ObjectType.SCHEMA, + "object_identifier": "package_name.stage_schema", + "role_to_grant": "app_role", + "role_to_use": "package_role", + }, + "grant usage on schema package_name.stage_schema to role app_role", + ), + ( + { + "privileges": ["read"], + "object_type": ObjectType.STAGE, + "object_identifier": "stage_fqn", + "role_to_grant": "app_role", + "role_to_use": None, + }, + "grant read on stage stage_fqn to role app_role", + ), + ], +) +def test_grant_privileges_to_role( + mock_use_role, + mock_execute_query, + args, + expected_query, +): + expected_use_objects = [(mock_use_role, mock.call(args["role_to_use"]))] + expected_execute_query = [(mock_execute_query, mock.call(expected_query))] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.grant_privileges_to_role(**args) + + +@pytest.mark.parametrize( + "args,expected_query", + [ + ( + {"name": "example_app", "role": "example_role"}, + r"show applications like 'EXAMPLE\\_APP'", + ), + ( + {"name": "nounderscores", "role": None}, + r"show applications like 'NOUNDERSCORES'", + ), + ], +) +def test_get_existing_app_info( + mock_use_role, mock_execute_query, args, expected_query, mock_cursor +): + expected_use_objects = [(mock_use_role, mock.call(args["role"]))] + + mock_cursor_results = [ + { + NAME_COL: "NOT_NAME", + }, + { + NAME_COL: args["name"].upper(), + }, + ] + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor(mock_cursor_results, []), + mock.call(expected_query), + ) + ] + ) + mock_execute_query.side_effect = side_effects + expected_execute_query = [ + (mock_execute_query, mock.call(expected_query, cursor_class=DictCursor)) + ] + + with assert_in_context(expected_use_objects, expected_execute_query): + result = sql_facade.get_existing_app_info(**args) + + assert result == {NAME_COL: args["name"].upper()} + + +def test_upgrade_application_unversioned( + mock_get_existing_app_info, + mock_use_warehouse, + mock_use_role, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call(f"alter application {app_name} upgrade using @{stage_fqn}"), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + debug_mode=None, + should_authorize_event_sharing=None, + role=role, + warehouse=warehouse, + ) + + +def test_upgrade_application_version_and_patch( + mock_get_existing_app_info, + mock_use_role, + mock_use_warehouse, + mock_get_app_properties, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call( + # make sure that "3" is quoted since that was a bug we found + f'alter application {app_name} upgrade using version "3" patch 2' + ), + ), + (None, mock.call(f"alter application {app_name} set debug_mode = True")), + ( + None, + mock.call( + f"alter application {app_name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = TRUE" + ), + ), + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.versioned_dev("3", 2), + stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=warehouse, + ) + + +def test_upgrade_application_from_release_directive( + mock_get_app_properties, + mock_get_existing_app_info, + mock_use_warehouse, + mock_use_role, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + mock_get_app_properties.return_value = { + COMMENT_COL: SPECIAL_COMMENT, + AUTHORIZE_TELEMETRY_COL: "true", + } + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call(f"alter application {app_name} upgrade "), + # not dev mode so no debug mode call + # authorize telemetry col is the same as arg, so no call + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=warehouse, + ) + + +def test_upgrade_application_converts_expected_programmingerrors_to_user_errors( + mock_get_existing_app_info, + mock_use_warehouse, + mock_use_role, + mock_execute_query, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + programming_error_message = "programming error message" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + msg=programming_error_message, + ), + mock.call(f"alter application {app_name} upgrade using @{stage_fqn}"), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(UserInputError) as err, + ): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno( + err, APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT + ) + assert err.match( + f"Failed to upgrade application {app_name} with the following error message:\n" + ) + assert err.match(programming_error_message) + + +def test_upgrade_application_special_message_for_event_sharing_error( + mock_get_existing_app_info, + mock_get_app_properties, + mock_use_warehouse, + mock_use_role, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + mock_get_app_properties.return_value = { + COMMENT_COL: SPECIAL_COMMENT, + AUTHORIZE_TELEMETRY_COL: "true", + } + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call(f"alter application {app_name} upgrade using version v1 "), + ), + (None, mock.call(f"alter application {app_name} set debug_mode = False")), + ( + ProgrammingError( + errno=CANNOT_DISABLE_MANDATORY_TELEMETRY, + ), + mock.call( + f"alter application {app_name} set AUTHORIZE_TELEMETRY_EVENT_SHARING = FALSE" + ), + ), + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(UserInputError) as err, + ): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.versioned_dev("v1"), + stage_fqn=stage_fqn, + debug_mode=False, + should_authorize_event_sharing=False, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno(err, CANNOT_DISABLE_MANDATORY_TELEMETRY) + assert err.match( + "Could not disable telemetry event sharing for the application because it contains mandatory events. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ) + + +def test_upgrade_application_converts_unexpected_programmingerrors_to_unclassified_errors( + mock_get_existing_app_info, + mock_use_warehouse, + mock_use_role, + mock_execute_query, +): + app_name = "test_app" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=SQL_COMPILATION_ERROR, + ), + mock.call(f"alter application {app_name} upgrade using @{stage_fqn}"), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(InvalidSQLError) as err, + ): + sql_facade.upgrade_application( + name=app_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno(err, SQL_COMPILATION_ERROR) + + +def test_create_application_with_minimal_clauses( + mock_use_warehouse, + mock_use_role, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call( + dedent( + f"""\ + create application {app_name} + from application package {pkg_name} + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + debug_mode=None, + should_authorize_event_sharing=None, + role=role, + warehouse=warehouse, + ) + + +def test_create_application_with_all_clauses( + mock_use_warehouse, + mock_use_role, + mock_execute_query, + mock_cursor, +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor([], []), + mock.call( + dedent( + f"""\ + create application {app_name} + from application package {pkg_name} using @{stage_fqn} debug_mode = True AUTHORIZE_TELEMETRY_EVENT_SHARING = TRUE + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.unversioned_dev(), + stage_fqn=stage_fqn, + debug_mode=True, + should_authorize_event_sharing=True, + role=role, + warehouse=warehouse, + ) + + +def test_create_application_converts_expected_programmingerrors_to_user_errors( + mock_use_warehouse, mock_use_role, mock_execute_query +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + programming_error_message = "programming error message" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT, + msg=programming_error_message, + ), + mock.call( + dedent( + f"""\ + create application {app_name} + from application package {pkg_name} + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(UserInputError) as err, + ): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + debug_mode=None, + should_authorize_event_sharing=None, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno( + err, APPLICATION_INSTANCE_FAILED_TO_RUN_SETUP_SCRIPT + ) + assert err.match( + f"Failed to create application {app_name} with the following error message:\n" + ) + assert err.match(programming_error_message) + + +def test_create_application_special_message_for_event_sharing_error( + mock_use_warehouse, mock_use_role, mock_execute_query +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=APPLICATION_REQUIRES_TELEMETRY_SHARING, + ), + mock.call( + dedent( + f"""\ + create application {app_name} + from application package {pkg_name} using version "3" patch 1 debug_mode = False AUTHORIZE_TELEMETRY_EVENT_SHARING = FALSE + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(UserInputError) as err, + ): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.versioned_dev("3", 1), + stage_fqn=stage_fqn, + debug_mode=False, + should_authorize_event_sharing=False, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno( + err, APPLICATION_REQUIRES_TELEMETRY_SHARING + ) + assert err.match( + "The application package requires event sharing to be authorized. Please set 'share_mandatory_events' to true in the application telemetry section of the project definition file." + ) + + +def test_create_application_converts_unexpected_programmingerrors_to_unclassified_errors( + mock_use_warehouse, mock_use_role, mock_execute_query +): + app_name = "test_app" + pkg_name = "test_pkg" + stage_fqn = "app_pkg.app_src.stage" + role = "test_role" + warehouse = "test_warehouse" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError( + errno=SQL_COMPILATION_ERROR, + ), + mock.call( + dedent( + f"""\ + create application {app_name} + from application package {pkg_name} + comment = {SPECIAL_COMMENT} + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + (mock_use_warehouse, mock.call(warehouse)), + ] + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with ( + assert_in_context(expected_use_objects, expected_execute_query), + pytest.raises(InvalidSQLError) as err, + ): + sql_facade.create_application( + name=app_name, + package_name=pkg_name, + install_method=SameAccountInstallMethod.release_directive(), + stage_fqn=stage_fqn, + debug_mode=None, + should_authorize_event_sharing=None, + role=role, + warehouse=warehouse, + ) + + assert_programmingerror_cause_with_errno(err, SQL_COMPILATION_ERROR) + + +@pytest.mark.parametrize( + "pkg_name, sanitized_pkg_name", + [("test_pkg", "test_pkg"), ("test.pkg", '"test.pkg"')], +) +def test_given_basic_pkg_when_create_application_package_then_success( + mock_execute_query, mock_use_role, pkg_name, sanitized_pkg_name +): + distribution = "INTERNAL" + role = "test_role" + + expected_use_objects = [(mock_use_role, mock.call(role))] + + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + dedent( + f"""\ + create application package {sanitized_pkg_name} + comment = {SPECIAL_COMMENT} + distribution = {distribution} + """ + ).strip() + ), + ) + ] + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.create_application_package(pkg_name, distribution, role=role) + + +@pytest.mark.parametrize("enable_release_channels", [True, False]) +def test_given_release_channels_when_create_application_package_then_success( + mock_execute_query, mock_use_role, enable_release_channels +): + package_name = "test_package" + distribution = "INTERNAL" + role = "test_role" + + expected_use_objects = [(mock_use_role, mock.call(role))] + + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + dedent( + f"""\ + create application package {package_name} + comment = {SPECIAL_COMMENT} + distribution = {distribution} + enable_release_channels = {str(enable_release_channels).lower()} + """ + ).strip() + ), + ) + ] + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.create_application_package( + package_name, + distribution, + role=role, + enable_release_channels=enable_release_channels, + ) + + +def test_given_programming_error_when_create_application_package_then_error( + mock_execute_query, + mock_use_role, +): + package_name = "test_package" + distribution = "INTERNAL" + role = "test_role" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError(), + mock.call( + dedent( + f"""\ + create application package {package_name} + comment = {SPECIAL_COMMENT} + distribution = {distribution} + """ + ).strip() + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + with pytest.raises(InvalidSQLError) as err: + sql_facade.create_application_package(package_name, distribution, role=role) + + assert "Failed to create application package" in str(err) + + +def test_given_privilege_error_when_create_application_package_then_raise_priv_error( + mock_execute_query, + mock_use_role, +): + package_name = "test_package" + distribution = "INTERNAL" + role = "test_role" + + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError(errno=INSUFFICIENT_PRIVILEGES), + mock.call( + dedent( + f"""\ + create application package {package_name} + comment = {SPECIAL_COMMENT} + distribution = {distribution} + """ + ).strip() + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + with pytest.raises(InsufficientPrivilegesError) as err: + sql_facade.create_application_package(package_name, distribution, role=role) + + assert "Insufficient privileges to create application package" in str(err) + + +@pytest.mark.parametrize( + "pkg_name, sanitized_pkg_name", + [("test_pkg", "test_pkg"), ("test.pkg", '"test.pkg"')], +) +@pytest.mark.parametrize("enable_release_channels", [True, False]) +def test_given_basic_pkg_when_update_application_package_properties_then_success( + mock_execute_query, + mock_use_role, + pkg_name, + sanitized_pkg_name, + enable_release_channels, +): + expected_use_objects = [(mock_use_role, mock.call(None))] + expected_execute_query = [ + ( + mock_execute_query, + mock.call( + dedent( + f"""\ + alter application package {sanitized_pkg_name} + set enable_release_channels = {str(enable_release_channels).lower()} + """ + ) + ), + ) + ] + with assert_in_context(expected_use_objects, expected_execute_query): + sql_facade.alter_application_package_properties( + pkg_name, enable_release_channels=enable_release_channels + ) + + +def test_given_no_enable_release_channel_flag_when_update_application_package_then_no_action( + mock_execute_query, +): + sql_facade.alter_application_package_properties("test_pkg", role="test_role") + + assert mock_execute_query.call_count == 0 + + +def test_given_programming_error_when_update_application_package_then_raise_sql_error( + mock_execute_query, mock_use_role +): + pkg_name = "test_pkg" + role = "test_role" + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError(), + mock.call( + dedent( + f"""\ + alter application package {pkg_name} + set enable_release_channels = True + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + with pytest.raises(InvalidSQLError) as err: + sql_facade.alter_application_package_properties( + pkg_name, enable_release_channels=True, role=role + ) + + assert "Failed to update enable_release_channels for application package" in str( + err + ) + + +def test_given_privilege_exception_when_update_application_package_then_raise_priv_error( + mock_execute_query, + mock_use_role, +): + pkg_name = "test_pkg" + role = "test_role" + side_effects, expected = mock_execute_helper( + [ + ( + ProgrammingError(errno=INSUFFICIENT_PRIVILEGES), + mock.call( + dedent( + f"""\ + alter application package {pkg_name} + set enable_release_channels = False + """ + ) + ), + ) + ] + ) + mock_execute_query.side_effect = side_effects + + with pytest.raises(InsufficientPrivilegesError) as err: + sql_facade.alter_application_package_properties( + pkg_name, enable_release_channels=False, role=role + ) + + assert ( + "Insufficient privileges update enable_release_channels for application package" + in str(err) + ) + + +expected_ui_params_query = "call system$bootstrap_data_request('CLIENT_PARAMS_INFO')" + + +def test_get_ui_parameter_with_value(mock_cursor): + with mock.patch.object(sql_facade, "_sql_executor") as mock_sql_executor: + execute_str_mock = mock_sql_executor._conn.execute_string # noqa: SLF001 + execute_str_mock.return_value = ( + None, + mock_cursor( + [ + ( + """\ + { + "clientParamsInfo": [{ + "name": "FEATURE_RELEASE_CHANNELS", + "value": true + }] + } + """, + ) + ], + [], + ), + ) + + assert ( + sql_facade.get_ui_parameter(UIParameter.NA_FEATURE_RELEASE_CHANNELS, False) + is True + ) + + execute_str_mock.assert_called_once_with(expected_ui_params_query) + + +def test_get_ui_parameter_with_empty_value_then_use_empty_value(mock_cursor): + with mock.patch.object(sql_facade, "_sql_executor") as mock_sql_executor: + execute_str_mock = mock_sql_executor._conn.execute_string # noqa: SLF001 + execute_str_mock.return_value = ( + None, + mock_cursor( + [ + ( + """\ + { + "clientParamsInfo": [{ + "name": "FEATURE_RELEASE_CHANNELS", + "value": "" + }] + } + """, + ) + ], + [], + ), + ) + + assert ( + sql_facade.get_ui_parameter(UIParameter.NA_FEATURE_RELEASE_CHANNELS, False) + == "" + ) + + execute_str_mock.assert_called_once_with(expected_ui_params_query) + + +def test_get_ui_parameter_with_no_value_then_use_default(mock_cursor): + with mock.patch.object(sql_facade, "_sql_executor") as mock_sql_executor: + execute_str_mock = mock_sql_executor._conn.execute_string # noqa: SLF001 + execute_str_mock.return_value = ( + None, + mock_cursor( + [ + ( + """\ + { + "clientParamsInfo": [] + } + """, + ) + ], + [], + ), + ) + + assert ( + sql_facade.get_ui_parameter(UIParameter.NA_FEATURE_RELEASE_CHANNELS, "any") + == "any" + ) + + execute_str_mock.assert_called_once_with(expected_ui_params_query) diff --git a/tests/nativeapp/test_teardown.py b/tests/nativeapp/test_teardown.py index 6566fc3ceb..a99b1052a2 100644 --- a/tests/nativeapp/test_teardown.py +++ b/tests/nativeapp/test_teardown.py @@ -50,13 +50,13 @@ from tests.nativeapp.patch_utils import mock_get_app_pkg_distribution_in_sf from tests.nativeapp.utils import ( APP_ENTITY_DROP_GENERIC_OBJECT, - APP_ENTITY_GET_EXISTING_APP_INFO, APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION, APP_ENTITY_MODULE, APP_PACKAGE_ENTITY_DROP_GENERIC_OBJECT, APP_PACKAGE_ENTITY_GET_EXISTING_APP_PKG_INFO, APP_PACKAGE_ENTITY_IS_DISTRIBUTION_SAME, SQL_EXECUTOR_EXECUTE, + SQL_FACADE_GET_EXISTING_APP_INFO, TYPER_CONFIRM, TYPER_PROMPT, mock_execute_helper, @@ -195,7 +195,7 @@ def test_drop_generic_object_failure_w_exception( # Test drop_application() when no application exists -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO, return_value=None) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO, return_value=None) @pytest.mark.parametrize( "auto_yes_param", [True, False], # This should have no effect on the test @@ -219,7 +219,7 @@ def test_drop_application_no_existing_application( # Test drop_application() when the current role is not allowed to drop it -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch( APP_ENTITY_DROP_GENERIC_OBJECT, side_effect=ProgrammingError( @@ -258,7 +258,7 @@ def test_drop_application_current_role_is_not_owner( # Test drop_application() successfully when it has special comment -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(APP_ENTITY_DROP_GENERIC_OBJECT, return_value=None) @mock.patch(APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION, return_value=[]) @pytest.mark.parametrize( @@ -370,7 +370,7 @@ def test_drop_application_has_special_comment_and_quoted_name( # Test drop_application() without special comment AND auto_yes is False AND should_drop is False -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(APP_ENTITY_DROP_GENERIC_OBJECT, return_value=None) @mock.patch(f"{APP_ENTITY_MODULE}.{TYPER_CONFIRM}", return_value=False) def test_drop_application_user_prohibits_drop( @@ -408,7 +408,7 @@ def test_drop_application_user_prohibits_drop( # Test drop_application() without special comment AND auto_yes is False AND should_drop is True # Test drop_application() without special comment AND auto_yes is True -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(APP_ENTITY_DROP_GENERIC_OBJECT, return_value=None) @mock.patch(f"{APP_ENTITY_MODULE}.{TYPER_CONFIRM}", return_value=True) @mock.patch(APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION, return_value=[]) @@ -448,7 +448,7 @@ def test_drop_application_user_allows_drop( # Test idempotent drop_application() -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(APP_ENTITY_DROP_GENERIC_OBJECT, return_value=None) @mock.patch(APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION, return_value=[]) @pytest.mark.parametrize( @@ -496,7 +496,6 @@ def test_drop_application_idempotent( def test_drop_package_no_existing_application( mock_get_existing_app_pkg_info, auto_yes_param, temp_dir ): - current_working_directory = os.getcwd() create_named_file( file_name="snowflake.yml", @@ -1096,7 +1095,7 @@ def test_drop_package_idempotent( @mock.patch(f"{APP_ENTITY_MODULE}.{TYPER_PROMPT}") -@mock.patch(APP_ENTITY_GET_EXISTING_APP_INFO) +@mock.patch(SQL_FACADE_GET_EXISTING_APP_INFO) @mock.patch(APP_ENTITY_DROP_GENERIC_OBJECT, return_value=None) @mock.patch(APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION) @pytest.mark.parametrize( diff --git a/tests/nativeapp/utils.py b/tests/nativeapp/utils.py index 76dd46d9ec..2eb4c852d1 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -19,6 +19,9 @@ from textwrap import dedent from typing import List, Set +import pytest +from snowflake.connector import ProgrammingError + from tests.nativeapp.factories import ProjectV10Factory TYPER_CONFIRM = "typer.confirm" @@ -36,7 +39,6 @@ APP_ENTITY_MODULE = "snowflake.cli._plugins.nativeapp.entities.application" APP_ENTITY = f"{APP_ENTITY_MODULE}.ApplicationEntity" -APP_ENTITY_GET_EXISTING_APP_INFO = f"{APP_ENTITY}.get_existing_app_info" APP_ENTITY_DROP_GENERIC_OBJECT = f"{APP_ENTITY_MODULE}.drop_generic_object" APP_ENTITY_GET_OBJECTS_OWNED_BY_APPLICATION = ( f"{APP_ENTITY}.get_objects_owned_by_application" @@ -60,6 +62,10 @@ f"{APP_PACKAGE_ENTITY}.verify_project_distribution" ) +CODE_GEN = "snowflake.cli._plugins.nativeapp.codegen" +TEMPLATE_PROCESSOR = f"{CODE_GEN}.templates.templates_processor" +ARTIFACT_PROCESSOR = f"{CODE_GEN}.artifact_processor" + SQL_EXECUTOR_EXECUTE = f"{API_MODULE}.sql_execution.BaseSqlExecutor.execute_query" SQL_EXECUTOR_EXECUTE_QUERIES = ( f"{API_MODULE}.sql_execution.BaseSqlExecutor.execute_queries" @@ -73,6 +79,16 @@ SQL_FACADE_STAGE_EXISTS = f"{SQL_FACADE}.stage_exists" SQL_FACADE_CREATE_SCHEMA = f"{SQL_FACADE}.create_schema" SQL_FACADE_CREATE_STAGE = f"{SQL_FACADE}.create_stage" +SQL_FACADE_CREATE_APPLICATION = f"{SQL_FACADE}.create_application" +SQL_FACADE_UPGRADE_APPLICATION = f"{SQL_FACADE}.upgrade_application" +SQL_FACADE_GET_EVENT_DEFINITIONS = f"{SQL_FACADE}.get_event_definitions" +SQL_FACADE_GET_EXISTING_APP_INFO = f"{SQL_FACADE}.get_existing_app_info" +SQL_FACADE_GRANT_PRIVILEGES_TO_ROLE = f"{SQL_FACADE}.grant_privileges_to_role" +SQL_FACADE_GET_UI_PARAMETER = f"{SQL_FACADE}.get_ui_parameter" +SQL_FACADE_ALTER_APP_PKG_PROPERTIES = ( + f"{SQL_FACADE}.alter_application_package_properties" +) +SQL_FACADE_CREATE_APP_PKG = f"{SQL_FACADE}.create_application_package" mock_snowflake_yml_file = dedent( """\ @@ -308,3 +324,15 @@ def use_integration_project(): "app/manifest.yml": manifest_contents, }, ) + + +def mock_side_effect_error_with_cause(err: Exception, cause: Exception): + with pytest.raises(type(err)) as side_effect: + raise err from cause + + return side_effect.value + + +def assert_programmingerror_cause_with_errno(err: pytest.ExceptionInfo, errno: int): + assert isinstance(err.value.__cause__, ProgrammingError) + assert err.value.__cause__.errno == errno diff --git a/tests/snowpark/__snapshots__/test_function.ambr b/tests/snowpark/__snapshots__/test_function.ambr index edde16c81f..77e6a834e7 100644 --- a/tests/snowpark/__snapshots__/test_function.ambr +++ b/tests/snowpark/__snapshots__/test_function.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_deploy_function_fully_qualified_name[ok] +# name: test_deploy_function_fully_qualified_name[snowpark_function_fully_qualified_name-snowpark.functions.5.name][ok] ''' Performing initial validation Checking remote state @@ -28,7 +28,36 @@ ''' # --- -# name: test_deploy_function_fully_qualified_name_duplicated_database[database error] +# name: test_deploy_function_fully_qualified_name[snowpark_function_fully_qualified_name_v2-entities.custom_database_custom_schema_fqn_function_error.identifier.name][ok] + ''' + Performing initial validation + Checking remote state + Preparing required stages and artifacts + Creating (if not exists) stage: dev_deployment + Uploading app.zip to @MockDatabase.MockSchema.dev_deployment/my_snowpark_project/ + Creating Snowpark entities + Creating function custom_database.custom_schema.fqn_function3 + Creating function custom_db.custom_schema.fqn_function + Creating function custom_db.custom_schema.database_function + Creating function custom_schema.fqn_function_only_schema + Creating function custom_db.PUBLIC.database_function + Creating function custom_schema.schema_function + +------------------------------------------------------------------------------+ + | object | type | status | + |---------------------------------------------------------+----------+---------| + | custom_database.custom_schema.fqn_function3(name | function | created | + | string) | | | + | custom_db.custom_schema.fqn_function(name string) | function | created | + | custom_db.custom_schema.database_function(name string) | function | created | + | MockDatabase.custom_schema.fqn_function_only_schema(nam | function | created | + | e string) | | | + | custom_db.MockSchema.database_function(name string) | function | created | + | MockDatabase.custom_schema.schema_function(name string) | function | created | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_function_fully_qualified_name_duplicated_database[snowpark_function_fully_qualified_name][database error] ''' Performing initial validation Checking remote state @@ -39,7 +68,29 @@ ''' # --- -# name: test_deploy_function_fully_qualified_name_duplicated_schema[schema error] +# name: test_deploy_function_fully_qualified_name_duplicated_database[snowpark_function_fully_qualified_name_v2][database error] + ''' + Performing initial validation + Checking remote state + +- Error ----------------------------------------------------------------------+ + | Database provided but name | + | 'custom_database.custom_schema.fqn_function_error' is fully qualified name. | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_function_fully_qualified_name_duplicated_schema[snowpark_function_fully_qualified_name-snowpark.functions.5.name][schema error] + ''' + Performing initial validation + Checking remote state + +- Error ----------------------------------------------------------------------+ + | Schema provided but name 'custom_schema.fqn_function_error' is fully | + | qualified name. | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_function_fully_qualified_name_duplicated_schema[snowpark_function_fully_qualified_name_v2-entities.custom_database_custom_schema_fqn_function_error.identifier.name][schema error] ''' Performing initial validation Checking remote state @@ -50,7 +101,17 @@ ''' # --- -# name: test_deploy_function_secrets_without_external_access +# name: test_deploy_function_secrets_without_external_access[snowpark_function_secrets_without_external_access] + ''' + Performing initial validation + Checking remote state + +- Error ----------------------------------------------------------------------+ + | func1 defined with secrets but without external integration. | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_function_secrets_without_external_access[snowpark_function_secrets_without_external_access_v2] ''' Performing initial validation Checking remote state diff --git a/tests/snowpark/__snapshots__/test_procedure.ambr b/tests/snowpark/__snapshots__/test_procedure.ambr index aab627199a..624b064cd5 100644 --- a/tests/snowpark/__snapshots__/test_procedure.ambr +++ b/tests/snowpark/__snapshots__/test_procedure.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_deploy_procedure_fails_if_integration_does_not_exists +# name: test_deploy_procedure_fails_if_integration_does_not_exists[snowpark_procedure_external_access] ''' Performing initial validation Checking remote state @@ -10,7 +10,18 @@ ''' # --- -# name: test_deploy_procedure_fails_if_object_exists_and_no_replace +# name: test_deploy_procedure_fails_if_integration_does_not_exists[snowpark_procedure_external_access_v2] + ''' + Performing initial validation + Checking remote state + +- Error ----------------------------------------------------------------------+ + | Following external access integration does not exists in Snowflake: | + | external_2 | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_procedure_fails_if_object_exists_and_no_replace[snowpark_procedures] ''' Performing initial validation Checking remote state @@ -22,7 +33,30 @@ ''' # --- -# name: test_deploy_procedure_fully_qualified_name[database error] +# name: test_deploy_procedure_fails_if_object_exists_and_no_replace[snowpark_procedures_v2] + ''' + Performing initial validation + Checking remote state + +- Error ----------------------------------------------------------------------+ + | Following objects already exists. Consider using --replace. | + | procedure: procedureName | + | procedure: test | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_procedure_fully_qualified_name[snowpark_procedure_fully_qualified_name][database error] + ''' + Performing initial validation + Checking remote state + +- Error ----------------------------------------------------------------------+ + | Database provided but name | + | 'custom_database.custom_schema.fqn_procedure_error' is fully qualified name. | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_procedure_fully_qualified_name[snowpark_procedure_fully_qualified_name_v2][database error] ''' Performing initial validation Checking remote state @@ -33,7 +67,7 @@ ''' # --- -# name: test_deploy_procedure_fully_qualified_name_duplicated_schema[schema error] +# name: test_deploy_procedure_fully_qualified_name_duplicated_schema[snowpark_procedure_fully_qualified_name-snowpark.procedures.5.name][schema error] ''' Performing initial validation Checking remote state @@ -44,7 +78,28 @@ ''' # --- -# name: test_deploy_procedure_secrets_without_external_access +# name: test_deploy_procedure_fully_qualified_name_duplicated_schema[snowpark_procedure_fully_qualified_name_v2-entities.custom_database_custom_schema_fqn_procedure_error.identifier.name][schema error] + ''' + Performing initial validation + Checking remote state + +- Error ----------------------------------------------------------------------+ + | Schema provided but name 'custom_schema.fqn_procedure_error' is fully | + | qualified name. | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_procedure_secrets_without_external_access[snowpark_procedure_secrets_without_external_access] + ''' + Performing initial validation + Checking remote state + +- Error ----------------------------------------------------------------------+ + | procedureName defined with secrets but without external integration. | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_procedure_secrets_without_external_access[snowpark_procedure_secrets_without_external_access_v2] ''' Performing initial validation Checking remote state diff --git a/tests/snowpark/test_function.py b/tests/snowpark/test_function.py index a6559e6bd7..180c3a9806 100644 --- a/tests/snowpark/test_function.py +++ b/tests/snowpark/test_function.py @@ -33,6 +33,9 @@ ) +@pytest.mark.parametrize( + "project_name", ["snowpark_functions", "snowpark_functions_v2"] +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager") @mock_session_has_warehouse @@ -42,13 +45,14 @@ def test_deploy_function( mock_ctx, runner, project_directory, + project_name, ): mock_object_manager.return_value.describe.side_effect = ProgrammingError( errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED ) ctx = mock_ctx() mock_connector.return_value = ctx - with project_directory("snowpark_functions") as project_dir: + with project_directory(project_name) as project_dir: result = runner.invoke( [ "snowpark", @@ -77,6 +81,10 @@ def test_deploy_function( ] +@pytest.mark.parametrize( + "project_name", + ["snowpark_function_external_access", "snowpark_function_external_access_v2"], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager") @mock_session_has_warehouse @@ -86,6 +94,7 @@ def test_deploy_function_with_external_access( mock_ctx, runner, project_directory, + project_name, ): mock_object_manager.return_value.show.return_value = [ {"name": "external_1", "type": "EXTERNAL_ACCESS"}, @@ -97,7 +106,7 @@ def test_deploy_function_with_external_access( ctx = mock_ctx() mock_connector.return_value = ctx - with project_directory("snowpark_function_external_access") as project_dir: + with project_directory(project_name) as project_dir: result = runner.invoke( [ "snowpark", @@ -128,6 +137,13 @@ def test_deploy_function_with_external_access( ] +@pytest.mark.parametrize( + "project_name", + [ + "snowpark_function_secrets_without_external_access", + "snowpark_function_secrets_without_external_access_v2", + ], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager") @mock_session_has_warehouse @@ -138,6 +154,7 @@ def test_deploy_function_secrets_without_external_access( mock_ctx, project_directory, os_agnostic_snapshot, + project_name, ): mock_object_manager.return_value.show.return_value = [ {"name": "external_1", "type": "EXTERNAL_ACCESS"}, @@ -146,7 +163,7 @@ def test_deploy_function_secrets_without_external_access( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_function_secrets_without_external_access"): + with project_directory(project_name): result = runner.invoke( [ "snowpark", @@ -158,6 +175,9 @@ def test_deploy_function_secrets_without_external_access( assert result.output == os_agnostic_snapshot +@pytest.mark.parametrize( + "project_name", ["snowpark_functions", "snowpark_functions_v2"] +) @mock.patch("snowflake.connector.connect") @mock_session_has_warehouse def test_deploy_function_no_changes( @@ -166,6 +186,7 @@ def test_deploy_function_no_changes( mock_ctx, mock_cursor, project_directory, + project_name, ): rows = [ ("packages", '["foo==1.2.3", "bar>=3.0.0"]'), @@ -182,6 +203,7 @@ def test_deploy_function_no_changes( mock_ctx, mock_cursor, project_directory, + project_name, "--replace", ) @@ -199,14 +221,13 @@ def test_deploy_function_no_changes( ] +@pytest.mark.parametrize( + "project_name", ["snowpark_functions", "snowpark_functions_v2"] +) @mock.patch("snowflake.connector.connect") @mock_session_has_warehouse def test_deploy_function_needs_update_because_packages_changes( - mock_connector, - runner, - mock_ctx, - mock_cursor, - project_directory, + mock_connector, runner, mock_ctx, mock_cursor, project_directory, project_name ): rows = [ ("packages", '["foo==1.2.3"]'), @@ -221,6 +242,7 @@ def test_deploy_function_needs_update_because_packages_changes( mock_ctx, mock_cursor, project_directory, + project_name, "--replace", ) @@ -250,14 +272,13 @@ def test_deploy_function_needs_update_because_packages_changes( ] +@pytest.mark.parametrize( + "project_name", ["snowpark_functions", "snowpark_functions_v2"] +) @mock.patch("snowflake.connector.connect") @mock_session_has_warehouse def test_deploy_function_needs_update_because_handler_changes( - mock_connector, - runner, - mock_ctx, - mock_cursor, - project_directory, + mock_connector, runner, mock_ctx, mock_cursor, project_directory, project_name ): rows = [ ("packages", '["foo==1.2.3", "bar>=3.0.0"]'), @@ -272,6 +293,7 @@ def test_deploy_function_needs_update_because_handler_changes( mock_ctx, mock_cursor, project_directory, + project_name, "--replace", ) @@ -302,6 +324,13 @@ def test_deploy_function_needs_update_because_handler_changes( ] +@pytest.mark.parametrize( + "project_name", + [ + "snowpark_function_fully_qualified_name", + "snowpark_function_fully_qualified_name_v2", + ], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -315,6 +344,7 @@ def test_deploy_function_fully_qualified_name_duplicated_database( project_directory, alter_snowflake_yml, os_agnostic_snapshot, + project_name, ): number_of_functions_in_project = 6 mock_om_describe.side_effect = [ @@ -323,11 +353,21 @@ def test_deploy_function_fully_qualified_name_duplicated_database( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_function_fully_qualified_name") as tmp_dir: + with project_directory(project_name) as tmp_dir: result = runner.invoke(["snowpark", "deploy"]) assert result.output == os_agnostic_snapshot(name="database error") +@pytest.mark.parametrize( + "project_name,path_in_project_file", + [ + ("snowpark_function_fully_qualified_name", "snowpark.functions.5.name"), + ( + "snowpark_function_fully_qualified_name_v2", + "entities.custom_database_custom_schema_fqn_function_error.identifier.name", + ), + ], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -341,6 +381,8 @@ def test_deploy_function_fully_qualified_name_duplicated_schema( project_directory, alter_snowflake_yml, os_agnostic_snapshot, + project_name, + path_in_project_file, ): number_of_functions_in_project = 6 mock_om_describe.side_effect = [ @@ -349,16 +391,26 @@ def test_deploy_function_fully_qualified_name_duplicated_schema( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_function_fully_qualified_name") as tmp_dir: + with project_directory(project_name) as tmp_dir: alter_snowflake_yml( tmp_dir / "snowflake.yml", - parameter_path="snowpark.functions.5.name", + parameter_path=path_in_project_file, value="custom_schema.fqn_function_error", ) result = runner.invoke(["snowpark", "deploy"]) assert result.output == os_agnostic_snapshot(name="schema error") +@pytest.mark.parametrize( + "project_name,parameter_path", + [ + ("snowpark_function_fully_qualified_name", "snowpark.functions.5.name"), + ( + "snowpark_function_fully_qualified_name_v2", + "entities.custom_database_custom_schema_fqn_function_error.identifier.name", + ), + ], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -372,6 +424,8 @@ def test_deploy_function_fully_qualified_name( project_directory, alter_snowflake_yml, os_agnostic_snapshot, + project_name, + parameter_path, ): number_of_functions_in_project = 6 mock_om_describe.side_effect = [ @@ -380,10 +434,10 @@ def test_deploy_function_fully_qualified_name( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_function_fully_qualified_name") as tmp_dir: + with project_directory(project_name) as tmp_dir: alter_snowflake_yml( tmp_dir / "snowflake.yml", - parameter_path="snowpark.functions.5.name", + parameter_path=parameter_path, value="fqn_function3", ) result = runner.invoke(["snowpark", "deploy"]) @@ -391,6 +445,21 @@ def test_deploy_function_fully_qualified_name( assert result.output == os_agnostic_snapshot(name="ok") +@pytest.mark.parametrize( + "project_name,signature_path,runtime_path", + [ + ( + "snowpark_functions", + "snowpark.functions.0.signature.0.", + "snowpark.functions.0.runtime", + ), + ( + "snowpark_functions_v2", + "entities.func1.signature.0.", + "entities.func1.runtime", + ), + ], +) @pytest.mark.parametrize( "parameter_type,default_value", [ @@ -413,23 +482,26 @@ def test_deploy_function_with_empty_default_value( alter_snowflake_yml, parameter_type, default_value, + project_name, + signature_path, + runtime_path, ): mock_object_manager.return_value.describe.side_effect = ProgrammingError( errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED ) ctx = mock_ctx() mock_connector.return_value = ctx - with project_directory("snowpark_functions") as project_dir: + with project_directory(project_name) as project_dir: snowflake_yml = project_dir / "snowflake.yml" for param, value in [("type", parameter_type), ("default", default_value)]: alter_snowflake_yml( snowflake_yml, - parameter_path=f"snowpark.functions.0.signature.0.{param}", + parameter_path=f"{signature_path}{param}", value=value, ) alter_snowflake_yml( snowflake_yml, - parameter_path=f"snowpark.functions.0.runtime", + parameter_path=runtime_path, value="3.10", ) result = runner.invoke( @@ -475,6 +547,7 @@ def _deploy_function( mock_ctx, mock_cursor, project_directory, + project_name, *args, ): ctx = mock_ctx(mock_cursor(rows=rows, columns=[])) @@ -489,7 +562,7 @@ def _deploy_function( ): om_describe.return_value = rows - with project_directory("snowpark_functions") as temp_dir: + with project_directory(project_name) as temp_dir: (Path(temp_dir) / "requirements.snowflake.txt").write_text( "foo==1.2.3\nbar>=3.0.0" ) diff --git a/tests/snowpark/test_procedure.py b/tests/snowpark/test_procedure.py index 84a23d0999..cefef10c29 100644 --- a/tests/snowpark/test_procedure.py +++ b/tests/snowpark/test_procedure.py @@ -52,6 +52,9 @@ def test_deploy_function_no_procedure(runner, project_directory): ) +@pytest.mark.parametrize( + "project_name", ["snowpark_procedures", "snowpark_procedures_v2"] +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -63,6 +66,7 @@ def test_deploy_procedure( runner, mock_ctx, project_directory, + project_name, ): mock_om_describe.side_effect = ProgrammingError( @@ -71,7 +75,7 @@ def test_deploy_procedure( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_procedures") as tmp: + with project_directory(project_name) as tmp: result = runner.invoke( [ "snowpark", @@ -116,6 +120,10 @@ def test_deploy_procedure( ] +@pytest.mark.parametrize( + "project_name", + ["snowpark_procedure_external_access", "snowpark_procedure_external_access_v2"], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -127,6 +135,7 @@ def test_deploy_procedure_with_external_access( runner, mock_ctx, project_directory, + project_name, ): mock_om_describe.side_effect = ProgrammingError( errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED @@ -139,7 +148,7 @@ def test_deploy_procedure_with_external_access( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_procedure_external_access") as project_dir: + with project_directory(project_name) as project_dir: result = runner.invoke( [ "snowpark", @@ -177,6 +186,13 @@ def test_deploy_procedure_with_external_access( ] +@pytest.mark.parametrize( + "project_name", + [ + "snowpark_procedure_secrets_without_external_access", + "snowpark_procedure_secrets_without_external_access_v2", + ], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -189,6 +205,7 @@ def test_deploy_procedure_secrets_without_external_access( mock_ctx, project_directory, os_agnostic_snapshot, + project_name, ): ctx = mock_ctx() mock_conn.return_value = ctx @@ -198,7 +215,7 @@ def test_deploy_procedure_secrets_without_external_access( {"name": "external_2", "type": "EXTERNAL_ACCESS"}, ] - with project_directory("snowpark_procedure_secrets_without_external_access"): + with project_directory(project_name): result = runner.invoke( [ "snowpark", @@ -211,6 +228,10 @@ def test_deploy_procedure_secrets_without_external_access( assert result.output == os_agnostic_snapshot +@pytest.mark.parametrize( + "project_name", + ["snowpark_procedure_external_access", "snowpark_procedure_external_access_v2"], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -223,6 +244,7 @@ def test_deploy_procedure_fails_if_integration_does_not_exists( mock_ctx, project_directory, os_agnostic_snapshot, + project_name, ): ctx = mock_ctx() mock_conn.return_value = ctx @@ -231,7 +253,7 @@ def test_deploy_procedure_fails_if_integration_does_not_exists( {"name": "external_1", "type": "EXTERNAL_ACCESS"}, ] - with project_directory("snowpark_procedure_external_access"): + with project_directory(project_name): result = runner.invoke( [ "snowpark", @@ -244,6 +266,9 @@ def test_deploy_procedure_fails_if_integration_does_not_exists( assert result.output == os_agnostic_snapshot +@pytest.mark.parametrize( + "project_name", ["snowpark_procedures", "snowpark_procedures_v2"] +) @mock.patch( "snowflake.cli._plugins.snowpark.commands._check_if_all_defined_integrations_exists" ) @@ -261,6 +286,7 @@ def test_deploy_procedure_fails_if_object_exists_and_no_replace( mock_ctx, project_directory, os_agnostic_snapshot, + project_name, ): mock_om_describe.return_value = mock_cursor( [ @@ -273,13 +299,16 @@ def test_deploy_procedure_fails_if_object_exists_and_no_replace( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_procedures"): + with project_directory(project_name): result = runner.invoke(["snowpark", "deploy"]) assert result.exit_code == 1 assert result.output == os_agnostic_snapshot +@pytest.mark.parametrize( + "project_name", ["snowpark_procedures", "snowpark_procedures_v2"] +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -293,6 +322,7 @@ def test_deploy_procedure_replace_nothing_to_update( mock_ctx, project_directory, caplog, + project_name, ): mock_om_describe.side_effect = [ mock_cursor( @@ -318,7 +348,7 @@ def test_deploy_procedure_replace_nothing_to_update( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_procedures"): + with project_directory(project_name): result = runner.invoke(["snowpark", "deploy", "--replace", "--format", "json"]) assert result.exit_code == 0, result.output @@ -336,6 +366,9 @@ def test_deploy_procedure_replace_nothing_to_update( ] +@pytest.mark.parametrize( + "project_name", ["snowpark_procedures", "snowpark_procedures_v2"] +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -348,6 +381,7 @@ def test_deploy_procedure_replace_updates_single_object( mock_cursor, mock_ctx, project_directory, + project_name, ): mock_om_describe.side_effect = [ mock_cursor( @@ -372,7 +406,7 @@ def test_deploy_procedure_replace_updates_single_object( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_procedures"): + with project_directory(project_name): result = runner.invoke(["snowpark", "deploy", "--replace", "--format", "json"]) assert result.exit_code == 0 @@ -390,6 +424,9 @@ def test_deploy_procedure_replace_updates_single_object( ] +@pytest.mark.parametrize( + "project_name", ["snowpark_procedures", "snowpark_procedures_v2"] +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -402,6 +439,7 @@ def test_deploy_procedure_replace_creates_missing_object( mock_cursor, mock_ctx, project_directory, + project_name, ): mock_om_describe.side_effect = [ mock_cursor( @@ -418,7 +456,7 @@ def test_deploy_procedure_replace_creates_missing_object( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_procedures"): + with project_directory(project_name): result = runner.invoke(["snowpark", "deploy", "--replace", "--format", "json"]) assert result.exit_code == 0 @@ -436,6 +474,13 @@ def test_deploy_procedure_replace_creates_missing_object( ] +@pytest.mark.parametrize( + "project_name", + [ + "snowpark_procedure_fully_qualified_name", + "snowpark_procedure_fully_qualified_name_v2", + ], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -449,6 +494,7 @@ def test_deploy_procedure_fully_qualified_name( project_directory, alter_snowflake_yml, os_agnostic_snapshot, + project_name, ): number_of_procedures_in_projects = 6 mock_om_describe.side_effect = [ @@ -457,11 +503,21 @@ def test_deploy_procedure_fully_qualified_name( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_procedure_fully_qualified_name") as tmp_dir: + with project_directory(project_name) as tmp_dir: result = runner.invoke(["snowpark", "deploy"]) assert result.output == os_agnostic_snapshot(name="database error") +@pytest.mark.parametrize( + "project_name,parameter_path", + [ + ("snowpark_procedure_fully_qualified_name", "snowpark.procedures.5.name"), + ( + "snowpark_procedure_fully_qualified_name_v2", + "entities.custom_database_custom_schema_fqn_procedure_error.identifier.name", + ), + ], +) @mock.patch("snowflake.connector.connect") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.describe") @mock.patch("snowflake.cli._plugins.snowpark.commands.ObjectManager.show") @@ -475,6 +531,8 @@ def test_deploy_procedure_fully_qualified_name_duplicated_schema( project_directory, alter_snowflake_yml, os_agnostic_snapshot, + project_name, + parameter_path, ): number_of_procedures_in_projects = 6 mock_om_describe.side_effect = [ @@ -483,10 +541,10 @@ def test_deploy_procedure_fully_qualified_name_duplicated_schema( ctx = mock_ctx() mock_conn.return_value = ctx - with project_directory("snowpark_procedure_fully_qualified_name") as tmp_dir: + with project_directory(project_name) as tmp_dir: alter_snowflake_yml( tmp_dir / "snowflake.yml", - parameter_path="snowpark.procedures.5.name", + parameter_path=parameter_path, value="custom_schema.fqn_procedure_error", ) result = runner.invoke(["snowpark", "deploy"]) diff --git a/tests/stage/test_stage_path.py b/tests/stage/test_stage_path.py index 9bc4fe42cc..21c5b2885c 100644 --- a/tests/stage/test_stage_path.py +++ b/tests/stage/test_stage_path.py @@ -1,6 +1,7 @@ from __future__ import annotations import pytest +from snowflake.cli._plugins.stage.manager import DefaultStagePathParts from snowflake.cli.api.stage_path import StagePath # (path, is_git_repo) @@ -168,3 +169,93 @@ def test_parent_path(path, is_git_repo): def test_root_path(stage_name, path): stage_path = StagePath.from_stage_str(path) assert stage_path.root_path() == StagePath.from_stage_str(f"@{stage_name}") + + +@pytest.mark.parametrize( + "input_path, path, full_path, schema, stage, stage_name", + [ + ( + "db.test_schema.test_stage", + "test_stage", + "db.test_schema.test_stage", + "test_schema", + "db.test_schema.test_stage", + "test_stage", + ), + ( + "db.test_schema.test_stage/subdir", + "test_stage/subdir", + "db.test_schema.test_stage/subdir", + "test_schema", + "db.test_schema.test_stage", + "test_stage", + ), + ( + "db.test_schema.test_stage/nested/dir", + "test_stage/nested/dir", + "db.test_schema.test_stage/nested/dir", + "test_schema", + "db.test_schema.test_stage", + "test_stage", + ), + ( + "test_schema.test_stage/nested/dir", + "test_stage/nested/dir", + "test_schema.test_stage/nested/dir", + "test_schema", + "test_schema.test_stage", + "test_stage", + ), + ( + "test_schema.test_stage/trailing/", + "test_stage/trailing", + "test_schema.test_stage/trailing", + "test_schema", + "test_schema.test_stage", + "test_stage", + ), + ( + "db.test_schema.test_stage/nested/trailing/", + "test_stage/nested/trailing", + "db.test_schema.test_stage/nested/trailing", + "test_schema", + "db.test_schema.test_stage", + "test_stage", + ), + ( + "test_stage/nested/trailing/", + "test_stage/nested/trailing", + "test_stage/nested/trailing", + None, + "test_stage", + "test_stage", + ), + ("test_stage/", "test_stage", "test_stage", None, "test_stage", "test_stage"), + ( + "test_stage/nested/dir", + "test_stage/nested/dir", + "test_stage/nested/dir", + None, + "test_stage", + "test_stage", + ), + ("test_stage", "test_stage", "test_stage", None, "test_stage", "test_stage"), + ( + "test_stage/dir/", + "test_stage/dir", + "test_stage/dir", + None, + "test_stage", + "test_stage", + ), + ], +) +def test_default_stage_path_parts( + input_path, path, full_path, schema, stage, stage_name +): + stage_path_parts = DefaultStagePathParts(input_path) + assert stage_path_parts.full_path == full_path + assert stage_path_parts.schema == schema + assert stage_path_parts.path == path + assert stage_path_parts.stage == stage + assert stage_path_parts.stage_name == stage_name diff --git a/tests/streamlit/__snapshots__/test_commands.ambr b/tests/streamlit/__snapshots__/test_commands.ambr index 0bcfb9c0e6..846562546d 100644 --- a/tests/streamlit/__snapshots__/test_commands.ambr +++ b/tests/streamlit/__snapshots__/test_commands.ambr @@ -7,6 +7,80 @@ | For field entities.my_streamlit you provided '{'artifacts': {'1': | | 'foo_bar.py'}}'. This caused: Unable to extract tag using discriminator | | 'type' | + | For field entities.my_streamlit you provided '{'artifacts': {'1': | + | 'foo_bar.py'}}'. This caused: Unable to extract tag using discriminator | + | 'type' | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_put_files_on_stage[example_streamlit-merge_definition1] + list([ + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.streamlit_stage')", + 'put file://streamlit_app.py @MockDatabase.MockSchema.streamlit_stage/test_streamlit auto_compress=false parallel=4 overwrite=True', + 'put file://environment.yml @MockDatabase.MockSchema.streamlit_stage/test_streamlit auto_compress=false parallel=4 overwrite=True', + 'put file://pages/* @MockDatabase.MockSchema.streamlit_stage/test_streamlit/pages auto_compress=false parallel=4 overwrite=True', + ''' + CREATE STREAMLIT IDENTIFIER('MockDatabase.MockSchema.test_streamlit') + ROOT_LOCATION = '@MockDatabase.MockSchema.streamlit_stage/test_streamlit' + MAIN_FILE = 'streamlit_app.py' + QUERY_WAREHOUSE = test_warehouse + TITLE = 'My Fancy Streamlit' + ''', + 'select system$get_snowsight_host()', + 'select current_account_name()', + ]) +# --- +# name: test_deploy_put_files_on_stage[example_streamlit_v2-merge_definition0] + list([ + "create stage if not exists IDENTIFIER('MockDatabase.MockSchema.streamlit_stage')", + 'put file://streamlit_app.py @MockDatabase.MockSchema.streamlit_stage/test_streamlit auto_compress=false parallel=4 overwrite=True', + ''' + CREATE STREAMLIT IDENTIFIER('MockDatabase.MockSchema.test_streamlit') + ROOT_LOCATION = '@MockDatabase.MockSchema.streamlit_stage/test_streamlit' + MAIN_FILE = 'streamlit_app.py' + QUERY_WAREHOUSE = test_warehouse + TITLE = 'My Fancy Streamlit' + ''', + 'select system$get_snowsight_host()', + 'select current_account_name()', + ]) +# --- +# name: test_deploy_streamlit_nonexisting_file[example_streamlit-opts0] + ''' + +- Error ----------------------------------------------------------------------+ + | Provided file foo.bar does not exist | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_streamlit_nonexisting_file[example_streamlit-opts1] + ''' + +- Error ----------------------------------------------------------------------+ + | Provided file foo.bar does not exist | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_streamlit_nonexisting_file[example_streamlit_v2-opts2] + ''' + +- Error ----------------------------------------------------------------------+ + | Streamlit test_streamlit already exist. If you want to replace it use | + | --replace flag. | + +------------------------------------------------------------------------------+ + + ''' +# --- +# name: test_deploy_streamlit_nonexisting_file[example_streamlit_v2-opts3] + ''' + +- Error ----------------------------------------------------------------------+ + | During evaluation of DefinitionV20 in project definition following errors | + | were encountered: | + | For field entities.test_streamlit.streamlit you provided '{'artifacts': | + | ['foo.bar'], 'identifier': {'name': 'test_streamlit'}, 'main_file': | + | 'streamlit_app.py', 'query_warehouse': 'test_warehouse', 'stage': | + | 'streamlit', 'title': 'My Fancy Streamlit', 'type': 'streamlit'}'. This | + | caused: Value error, Specified artifact foo.bar does not exist locally. | +------------------------------------------------------------------------------+ ''' diff --git a/tests/streamlit/test_commands.py b/tests/streamlit/test_commands.py index deb6701695..95d8878fd1 100644 --- a/tests/streamlit/test_commands.py +++ b/tests/streamlit/test_commands.py @@ -66,7 +66,7 @@ def _put_query(source: str, dest: str): @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_only_streamlit_file( @@ -121,7 +121,7 @@ def test_deploy_only_streamlit_file( @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_only_streamlit_file_no_stage( @@ -175,7 +175,7 @@ def test_deploy_only_streamlit_file_no_stage( @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_with_empty_pages( @@ -231,7 +231,7 @@ def test_deploy_with_empty_pages( @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_only_streamlit_file_replace( @@ -298,11 +298,12 @@ def test_artifacts_must_exist( assert result.output == snapshot +@pytest.mark.parametrize("project_name", ["example_streamlit_v2", "example_streamlit"]) @mock.patch("snowflake.cli._plugins.streamlit.commands.typer") @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_launch_browser( @@ -313,6 +314,7 @@ def test_deploy_launch_browser( runner, mock_ctx, project_directory, + project_name, ): ctx = mock_ctx( mock_cursor( @@ -324,7 +326,7 @@ def test_deploy_launch_browser( ) mock_connector.return_value = ctx - with project_directory("example_streamlit"): + with project_directory(project_name): result = runner.invoke(["streamlit", "deploy", "--open"]) assert result.exit_code == 0, result.output @@ -334,14 +336,22 @@ def test_deploy_launch_browser( ) +@pytest.mark.parametrize("project_name", ["example_streamlit_v2", "example_streamlit"]) @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_streamlit_and_environment_files( - mock_param, mock_connector, mock_cursor, runner, mock_ctx, project_directory + mock_param, + mock_connector, + mock_cursor, + runner, + mock_ctx, + alter_snowflake_yml, + project_directory, + project_name, ): ctx = mock_ctx( mock_cursor( @@ -354,8 +364,14 @@ def test_deploy_streamlit_and_environment_files( ) mock_connector.return_value = ctx - with project_directory("example_streamlit") as pdir: + with project_directory(project_name) as pdir: shutil.rmtree(pdir / "pages") + if project_name == "example_streamlit_v2": + alter_snowflake_yml( + pdir / "snowflake.yml", + parameter_path="entities.test_streamlit.artifacts", + value=["streamlit_app.py", "environment.yml"], + ) result = runner.invoke(["streamlit", "deploy"]) @@ -379,14 +395,22 @@ def test_deploy_streamlit_and_environment_files( ] +@pytest.mark.parametrize("project_name", ["example_streamlit_v2", "example_streamlit"]) @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_streamlit_and_pages_files( - mock_param, mock_connector, mock_cursor, runner, mock_ctx, project_directory + mock_param, + mock_connector, + mock_cursor, + runner, + mock_ctx, + alter_snowflake_yml, + project_directory, + project_name, ): ctx = mock_ctx( mock_cursor( @@ -399,8 +423,14 @@ def test_deploy_streamlit_and_pages_files( ) mock_connector.return_value = ctx - with project_directory("example_streamlit") as pdir: + with project_directory(project_name) as pdir: (pdir / "environment.yml").unlink() + if project_name == "example_streamlit_v2": + alter_snowflake_yml( + pdir / "snowflake.yml", + parameter_path="entities.test_streamlit.artifacts", + value=["streamlit_app.py", "pages/"], + ) result = runner.invoke(["streamlit", "deploy"]) root_path = f"@MockDatabase.MockSchema.streamlit/{STREAMLIT_NAME}" @@ -423,14 +453,23 @@ def test_deploy_streamlit_and_pages_files( ] +@pytest.mark.parametrize( + "project_name", ["streamlit_full_definition_v2", "streamlit_full_definition"] +) @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_all_streamlit_files( - mock_param, mock_connector, mock_cursor, runner, mock_ctx, project_directory + mock_param, + mock_connector, + mock_cursor, + runner, + mock_ctx, + project_directory, + project_name, ): ctx = mock_ctx( mock_cursor( @@ -443,7 +482,7 @@ def test_deploy_all_streamlit_files( ) mock_connector.return_value = ctx - with project_directory("streamlit_full_definition"): + with project_directory(project_name): result = runner.invoke(["streamlit", "deploy"]) root_path = f"@MockDatabase.MockSchema.streamlit/{STREAMLIT_NAME}" @@ -468,14 +507,39 @@ def test_deploy_all_streamlit_files( ] +@pytest.mark.parametrize( + "project_name, merge_definition", + [ + ( + "example_streamlit_v2", + { + "entities": { + "test_streamlit": { + "stage": "streamlit_stage", + "artifacts": ["streamlit_app.py", "environment.yml", "pages"], + } + } + }, + ), + ("example_streamlit", {"streamlit": {"stage": "streamlit_stage"}}), + ], +) @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_put_files_on_stage( - mock_param, mock_connector, mock_cursor, runner, mock_ctx, project_directory + mock_param, + mock_connector, + mock_cursor, + runner, + mock_ctx, + alter_snowflake_yml, + project_directory, + project_name, + merge_definition, ): ctx = mock_ctx( mock_cursor( @@ -489,8 +553,8 @@ def test_deploy_put_files_on_stage( mock_connector.return_value = ctx with project_directory( - "example_streamlit", - merge_project_definition={"streamlit": {"stage": "streamlit_stage"}}, + project_name, + merge_project_definition=merge_definition, ): result = runner.invoke(["streamlit", "deploy"]) @@ -503,26 +567,36 @@ def test_deploy_put_files_on_stage( _put_query("pages/*", f"{root_path}/pages"), dedent( f""" - CREATE STREAMLIT IDENTIFIER('MockDatabase.MockSchema.{STREAMLIT_NAME}') - ROOT_LOCATION = '@MockDatabase.MockSchema.streamlit_stage/{STREAMLIT_NAME}' - MAIN_FILE = 'streamlit_app.py' - QUERY_WAREHOUSE = test_warehouse - TITLE = 'My Fancy Streamlit' - """ + CREATE STREAMLIT IDENTIFIER('MockDatabase.MockSchema.{STREAMLIT_NAME}') + ROOT_LOCATION = '@MockDatabase.MockSchema.streamlit_stage/{STREAMLIT_NAME}' + MAIN_FILE = 'streamlit_app.py' + QUERY_WAREHOUSE = test_warehouse + TITLE = 'My Fancy Streamlit' + """ ).strip(), f"select system$get_snowsight_host()", f"select current_account_name()", ] +@pytest.mark.parametrize( + "project_name", + ["example_streamlit_no_defaults", "example_streamlit_no_defaults_v2"], +) @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_all_streamlit_files_not_defaults( - mock_param, mock_connector, mock_cursor, runner, mock_ctx, project_directory + mock_param, + mock_connector, + mock_cursor, + runner, + mock_ctx, + project_directory, + project_name, ): ctx = mock_ctx( mock_cursor( @@ -535,7 +609,7 @@ def test_deploy_all_streamlit_files_not_defaults( ) mock_connector.return_value = ctx - with project_directory("example_streamlit_no_defaults"): + with project_directory(project_name): result = runner.invoke(["streamlit", "deploy"]) root_path = f"@MockDatabase.MockSchema.streamlit_stage/{STREAMLIT_NAME}" @@ -558,12 +632,13 @@ def test_deploy_all_streamlit_files_not_defaults( ] +@pytest.mark.parametrize("project_name", ["example_streamlit", "example_streamlit_v2"]) @mock.patch("snowflake.connector.connect") @pytest.mark.parametrize("enable_streamlit_versioned_stage", [True, False]) @pytest.mark.parametrize("enable_streamlit_no_checkouts", [True, False]) @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_streamlit_main_and_pages_files_experimental( @@ -572,9 +647,12 @@ def test_deploy_streamlit_main_and_pages_files_experimental( mock_cursor, runner, mock_ctx, + alter_snowflake_yml, project_directory, + os_agnostic_snapshot, enable_streamlit_versioned_stage, enable_streamlit_no_checkouts, + project_name, ): ctx = mock_ctx( mock_cursor( @@ -597,7 +675,13 @@ def test_deploy_streamlit_main_and_pages_files_experimental( return_value=enable_streamlit_no_checkouts, ), ): - with project_directory("example_streamlit"): + with project_directory(project_name) as pdir: + if project_name == "example_streamlit_v2": + alter_snowflake_yml( + pdir / "snowflake.yml", + parameter_path="entities.test_streamlit.artifacts", + value=["streamlit_app.py", "environment.yml", "pages"], + ) result = runner.invoke(["streamlit", "deploy", "--experimental"]) if enable_streamlit_versioned_stage: @@ -620,11 +704,11 @@ def test_deploy_streamlit_main_and_pages_files_experimental( for cmd in [ dedent( f""" - CREATE STREAMLIT IF NOT EXISTS IDENTIFIER('MockDatabase.MockSchema.{STREAMLIT_NAME}') - MAIN_FILE = 'streamlit_app.py' - QUERY_WAREHOUSE = test_warehouse - TITLE = 'My Fancy Streamlit' - """ + CREATE STREAMLIT IF NOT EXISTS IDENTIFIER('MockDatabase.MockSchema.{STREAMLIT_NAME}') + MAIN_FILE = 'streamlit_app.py' + QUERY_WAREHOUSE = test_warehouse + TITLE = 'My Fancy Streamlit' + """ ).strip(), post_create_command, _put_query("streamlit_app.py", root_path), @@ -637,10 +721,11 @@ def test_deploy_streamlit_main_and_pages_files_experimental( ] +@pytest.mark.parametrize("project_name", ["example_streamlit", "example_streamlit_v2"]) @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_streamlit_main_and_pages_files_experimental_double_deploy( @@ -649,7 +734,9 @@ def test_deploy_streamlit_main_and_pages_files_experimental_double_deploy( mock_cursor, runner, mock_ctx, + alter_snowflake_yml, project_directory, + project_name, ): ctx = mock_ctx( mock_cursor( @@ -662,7 +749,13 @@ def test_deploy_streamlit_main_and_pages_files_experimental_double_deploy( ) mock_connector.return_value = ctx - with project_directory("example_streamlit"): + with project_directory(project_name) as pdir: + if project_name == "example_streamlit_v2": + alter_snowflake_yml( + pdir / "snowflake.yml", + parameter_path="entities.test_streamlit.artifacts", + value=["streamlit_app.py", "environment.yml", "pages"], + ) result1 = runner.invoke(["streamlit", "deploy", "--experimental"]) assert result1.exit_code == 0, result1.output @@ -678,7 +771,13 @@ def test_deploy_streamlit_main_and_pages_files_experimental_double_deploy( ) ctx.queries = [] - with project_directory("example_streamlit"): + with project_directory(project_name) as pdir: + if project_name == "example_streamlit_v2": + alter_snowflake_yml( + pdir / "snowflake.yml", + parameter_path="entities.test_streamlit.artifacts", + value=["streamlit_app.py", "environment.yml", "pages"], + ) result2 = runner.invoke(["streamlit", "deploy", "--experimental"]) assert result2.exit_code == 0, result2.output @@ -703,11 +802,14 @@ def test_deploy_streamlit_main_and_pages_files_experimental_double_deploy( ] +@pytest.mark.parametrize( + "project_name", ["example_streamlit_no_stage", "example_streamlit_no_stage_v2"] +) @mock.patch("snowflake.connector.connect") @pytest.mark.parametrize("enable_streamlit_versioned_stage", [True, False]) @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_streamlit_main_and_pages_files_experimental_no_stage( @@ -716,8 +818,10 @@ def test_deploy_streamlit_main_and_pages_files_experimental_no_stage( mock_cursor, runner, mock_ctx, + alter_snowflake_yml, project_directory, enable_streamlit_versioned_stage, + project_name, ): ctx = mock_ctx( mock_cursor( @@ -734,7 +838,8 @@ def test_deploy_streamlit_main_and_pages_files_experimental_no_stage( "snowflake.cli.api.feature_flags.FeatureFlag.ENABLE_STREAMLIT_VERSIONED_STAGE.is_enabled", return_value=enable_streamlit_versioned_stage, ): - with project_directory("example_streamlit_no_stage"): + with project_directory(project_name): + result = runner.invoke(["streamlit", "deploy", "--experimental"]) if enable_streamlit_versioned_stage: @@ -766,14 +871,22 @@ def test_deploy_streamlit_main_and_pages_files_experimental_no_stage( ] +@pytest.mark.parametrize("project_name", ["example_streamlit", "example_streamlit_v2"]) @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) @mock_streamlit_exists def test_deploy_streamlit_main_and_pages_files_experimental_replace( - mock_param, mock_connector, mock_cursor, runner, mock_ctx, project_directory + mock_param, + mock_connector, + mock_cursor, + runner, + mock_ctx, + alter_snowflake_yml, + project_directory, + project_name, ): ctx = mock_ctx( mock_cursor( @@ -786,7 +899,13 @@ def test_deploy_streamlit_main_and_pages_files_experimental_replace( ) mock_connector.return_value = ctx - with project_directory("example_streamlit"): + with project_directory(project_name) as pdir: + if project_name == "example_streamlit_v2": + alter_snowflake_yml( + pdir / "snowflake.yml", + parameter_path="entities.test_streamlit.artifacts", + value=["streamlit_app.py", "environment.yml", "pages/"], + ) result = runner.invoke(["streamlit", "deploy", "--experimental", "--replace"]) root_path = f"@streamlit/MockDatabase.MockSchema.{STREAMLIT_NAME}/default_checkout" @@ -810,27 +929,32 @@ def test_deploy_streamlit_main_and_pages_files_experimental_replace( @pytest.mark.parametrize( - "opts", + "project_name,opts", [ - ("pages_dir", "foo/bar"), - ("env_file", "foo.yml"), + ("example_streamlit", {"streamlit": {"pages_dir": "foo.bar"}}), + ("example_streamlit", {"streamlit": {"env_file": "foo.bar"}}), + ( + "example_streamlit_v2", + {"entities": {"test_streamlit": {"pages_dir": "foo.bar"}}}, + ), + ( + "example_streamlit_v2", + {"entities": {"test_streamlit": {"artifacts": ["foo.bar"]}}}, + ), ], ) @mock.patch("snowflake.connector.connect") def test_deploy_streamlit_nonexisting_file( - mock_connector, runner, mock_ctx, project_directory, opts + mock_connector, runner, mock_ctx, snapshot, project_directory, opts, project_name ): ctx = mock_ctx() mock_connector.return_value = ctx - with project_directory( - "example_streamlit", merge_project_definition={"streamlit": {opts[0]: opts[1]}} - ): + with project_directory(project_name, merge_project_definition=opts): result = runner.invoke(["streamlit", "deploy"]) - assert f"Provided file {opts[1]} does not exist" in result.output.replace( - "\\", "/" - ) + assert result.exit_code == 1 + assert result.output == snapshot @mock.patch("snowflake.connector.connect") @@ -862,7 +986,7 @@ def test_drop_streamlit(mock_connector, runner, mock_ctx): @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) def test_get_streamlit_url(mock_param, mock_connector, mock_cursor, runner, mock_ctx): ctx = mock_ctx( @@ -944,7 +1068,7 @@ def test_multiple_streamlit_raise_error_if_multiple_entities( @mock.patch("snowflake.connector.connect") @mock.patch( GET_UI_PARAMETERS, - return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "false"}, + return_value={UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: False}, ) def test_deploy_streamlit_with_comment_v2( mock_param, mock_connector, mock_cursor, runner, mock_ctx, project_directory diff --git a/tests/test_connection.py b/tests/test_connection.py index 8345c1c3cd..ecb81f6da2 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1219,16 +1219,22 @@ def test_generate_jwt_with_passphrase( result.output == "Enter private key file password (press enter for empty) []: \nfunny token\n" ) - mocked_get_token.has_calls( - mock.call( - user="FooBar", account="account1", privatekey_path=str(f), key_password=None - ), - mock.call( - user="FooBar", - account="account1", - privatekey_path=str(f), - key_password=passphrase, - ), + mocked_get_token.assert_has_calls( + [ + mock.call( + user="FooBar", + account="account1", + privatekey_path=str(f), + key_password=None, + ), + mock.call( + user="FooBar", + account="account1", + privatekey_path=str(f), + key_password=passphrase, + ), + ], + any_order=True, ) diff --git a/tests/test_data/projects/empty_project/snowflake.yml b/tests/test_data/projects/empty_project/snowflake.yml index 99305157d5..498d7c1ace 100644 --- a/tests/test_data/projects/empty_project/snowflake.yml +++ b/tests/test_data/projects/empty_project/snowflake.yml @@ -1 +1 @@ -definition_version: 1 +definition_version: 2 diff --git a/tests/test_data/projects/example_streamlit_no_defaults_v2/main.py b/tests/test_data/projects/example_streamlit_no_defaults_v2/main.py new file mode 100644 index 0000000000..b699538d7e --- /dev/null +++ b/tests/test_data/projects/example_streamlit_no_defaults_v2/main.py @@ -0,0 +1,3 @@ +import streamlit as st + +st.title("Example streamlit app") diff --git a/tests/test_data/projects/example_streamlit_no_defaults_v2/snowflake.yml b/tests/test_data/projects/example_streamlit_no_defaults_v2/snowflake.yml new file mode 100644 index 0000000000..d38c152eb9 --- /dev/null +++ b/tests/test_data/projects/example_streamlit_no_defaults_v2/snowflake.yml @@ -0,0 +1,14 @@ +definition_version: '2' +entities: + test_streamlit: + identifier: + name: test_streamlit + type: streamlit + query_warehouse: streamlit_warehouse + main_file: main.py + pages_dir: streamlit_pages + stage: streamlit_stage + artifacts: + - main.py + - streamlit_environment.yml + - streamlit_pages diff --git a/tests/test_data/projects/example_streamlit_no_defaults_v2/snowflake_V1.yml b/tests/test_data/projects/example_streamlit_no_defaults_v2/snowflake_V1.yml new file mode 100644 index 0000000000..2b5306a8b8 --- /dev/null +++ b/tests/test_data/projects/example_streamlit_no_defaults_v2/snowflake_V1.yml @@ -0,0 +1,8 @@ +definition_version: 1 +streamlit: + name: test_streamlit + main_file: main.py + stage: streamlit_stage + query_warehouse: streamlit_warehouse + env_file: streamlit_environment.yml + pages_dir: streamlit_pages diff --git a/tests/test_data/projects/example_streamlit_no_defaults_v2/streamlit_environment.yml b/tests/test_data/projects/example_streamlit_no_defaults_v2/streamlit_environment.yml new file mode 100644 index 0000000000..ac8feac3e8 --- /dev/null +++ b/tests/test_data/projects/example_streamlit_no_defaults_v2/streamlit_environment.yml @@ -0,0 +1,5 @@ +name: sf_env +channels: + - snowflake +dependencies: + - pandas diff --git a/tests/test_data/projects/example_streamlit_no_defaults_v2/streamlit_pages/first_page.py b/tests/test_data/projects/example_streamlit_no_defaults_v2/streamlit_pages/first_page.py new file mode 100644 index 0000000000..bc3ecbccba --- /dev/null +++ b/tests/test_data/projects/example_streamlit_no_defaults_v2/streamlit_pages/first_page.py @@ -0,0 +1,3 @@ +import streamlit as st + +st.title("Example page") diff --git a/tests/test_data/projects/example_streamlit_no_stage_v2/environment.yml b/tests/test_data/projects/example_streamlit_no_stage_v2/environment.yml new file mode 100644 index 0000000000..ac8feac3e8 --- /dev/null +++ b/tests/test_data/projects/example_streamlit_no_stage_v2/environment.yml @@ -0,0 +1,5 @@ +name: sf_env +channels: + - snowflake +dependencies: + - pandas diff --git a/tests/test_data/projects/example_streamlit_no_stage_v2/pages/my_page.py b/tests/test_data/projects/example_streamlit_no_stage_v2/pages/my_page.py new file mode 100644 index 0000000000..bc3ecbccba --- /dev/null +++ b/tests/test_data/projects/example_streamlit_no_stage_v2/pages/my_page.py @@ -0,0 +1,3 @@ +import streamlit as st + +st.title("Example page") diff --git a/tests/test_data/projects/example_streamlit_no_stage_v2/snowflake.yml b/tests/test_data/projects/example_streamlit_no_stage_v2/snowflake.yml new file mode 100644 index 0000000000..608078a1a1 --- /dev/null +++ b/tests/test_data/projects/example_streamlit_no_stage_v2/snowflake.yml @@ -0,0 +1,14 @@ +definition_version: '2' +entities: + test_streamlit: + identifier: + name: test_streamlit + type: streamlit + query_warehouse: test_warehouse + main_file: streamlit_app.py + pages_dir: None + stage: streamlit + artifacts: + - streamlit_app.py + - environment.yml + - pages diff --git a/tests/test_data/projects/example_streamlit_no_stage_v2/streamlit_app.py b/tests/test_data/projects/example_streamlit_no_stage_v2/streamlit_app.py new file mode 100644 index 0000000000..b699538d7e --- /dev/null +++ b/tests/test_data/projects/example_streamlit_no_stage_v2/streamlit_app.py @@ -0,0 +1,3 @@ +import streamlit as st + +st.title("Example streamlit app") diff --git a/tests/test_data/projects/example_streamlit_v2/snowflake.yml b/tests/test_data/projects/example_streamlit_v2/snowflake.yml index 607b94bd0a..27205e6595 100644 --- a/tests/test_data/projects/example_streamlit_v2/snowflake.yml +++ b/tests/test_data/projects/example_streamlit_v2/snowflake.yml @@ -1,4 +1,4 @@ -definition_version: 2 +definition_version: '2' entities: test_streamlit: type: "streamlit" @@ -7,8 +7,6 @@ entities: stage: streamlit query_warehouse: xsmall main_file: streamlit_app.py + stage: streamlit artifacts: - streamlit_app.py - - utils/utils.py - - pages/ - - environment.yml diff --git a/tests/test_data/projects/snowpark_function_external_access_v2/app.py b/tests/test_data/projects/snowpark_function_external_access_v2/app.py new file mode 100644 index 0000000000..37c1ed1f96 --- /dev/null +++ b/tests/test_data/projects/snowpark_function_external_access_v2/app.py @@ -0,0 +1,2 @@ +def hello(name: str) -> str: + return f"Hello {name}!" diff --git a/tests/test_data/projects/snowpark_function_external_access_v2/requirements.txt b/tests/test_data/projects/snowpark_function_external_access_v2/requirements.txt new file mode 100644 index 0000000000..ed706cf032 --- /dev/null +++ b/tests/test_data/projects/snowpark_function_external_access_v2/requirements.txt @@ -0,0 +1 @@ +snowflake-snowpark-python diff --git a/tests/test_data/projects/snowpark_function_external_access_v2/snowflake.yml b/tests/test_data/projects/snowpark_function_external_access_v2/snowflake.yml new file mode 100644 index 0000000000..fa6a9d1a4c --- /dev/null +++ b/tests/test_data/projects/snowpark_function_external_access_v2/snowflake.yml @@ -0,0 +1,20 @@ +definition_version: 1 +snowpark: + project_name: "my_snowpark_project" + stage_name: "dev_deployment" + src: "app.py" + functions: + - name: func1 + handler: "app.func1_handler" + signature: + - name: "a" + type: "string" + - name: "b" + type: "variant" + returns: string + external_access_integrations: + - "external_1" + - "external_2" + secrets: + cred: "cred_name" + other: "other_name" diff --git a/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/app.zip b/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/app.zip new file mode 100644 index 0000000000..a732567ccf Binary files /dev/null and b/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/app.zip differ diff --git a/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/app/app.py b/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/app/app.py new file mode 100644 index 0000000000..b977042f0f --- /dev/null +++ b/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/app/app.py @@ -0,0 +1,5 @@ +from __future__ import annotations + + +def hello_function(name: str) -> str: + return f"Hello {name}!" diff --git a/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/requirements.txt b/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/requirements.txt new file mode 100644 index 0000000000..e07b4c8561 --- /dev/null +++ b/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/requirements.txt @@ -0,0 +1,2 @@ +snowflake-snowpark-python +#snowcli # for local development diff --git a/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/snowflake.yml b/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/snowflake.yml new file mode 100644 index 0000000000..17ce73cfe8 --- /dev/null +++ b/tests/test_data/projects/snowpark_function_fully_qualified_name_v2/snowflake.yml @@ -0,0 +1,127 @@ +definition_version: '2' +entities: + custom_db_custom_schema_fqn_function: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: custom_db.custom_schema.fqn_function + handler: app.hello_function + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: function + custom_schema_fqn_function_only_schema: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: custom_schema.fqn_function_only_schema + handler: app.hello_function + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: function + schema_function: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: schema_function + schema: custom_schema + handler: app.hello_function + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: function + database_function: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: database_function + database: custom_db + handler: app.hello_function + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: function + custom_schema_database_function: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: custom_schema.database_function + database: custom_db + handler: app.hello_function + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: function + custom_database_custom_schema_fqn_function_error: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: custom_database.custom_schema.fqn_function_error + schema: custom_schema + database: custom_database + handler: app.hello_function + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: function +mixins: + snowpark_shared: + stage: dev_deployment + artifacts: + - src: app/ + dest: my_snowpark_project diff --git a/tests/test_data/projects/snowpark_function_secrets_without_external_access_v2/app.py b/tests/test_data/projects/snowpark_function_secrets_without_external_access_v2/app.py new file mode 100644 index 0000000000..0c336bf90f --- /dev/null +++ b/tests/test_data/projects/snowpark_function_secrets_without_external_access_v2/app.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import sys + + +def hello(name: str) -> str: + return f"Hello {name}!" + + +# For local debugging. Be aware you may need to type-convert arguments if +# you add input parameters +if __name__ == "__main__": + if len(sys.argv) > 1: + print(hello(sys.argv[1])) # type: ignore + else: + print(hello("world")) diff --git a/tests/test_data/projects/snowpark_function_secrets_without_external_access_v2/snowflake.yml b/tests/test_data/projects/snowpark_function_secrets_without_external_access_v2/snowflake.yml new file mode 100644 index 0000000000..966b8a7f5d --- /dev/null +++ b/tests/test_data/projects/snowpark_function_secrets_without_external_access_v2/snowflake.yml @@ -0,0 +1,31 @@ +definition_version: '2' +entities: + func1: + imports: [] + external_access_integrations: [] + secrets: + cred: cred_name + other: other_name + meta: + use_mixins: + - snowpark_shared + identifier: + name: func1 + handler: app.func1_handler + returns: string + signature: + - name: a + type: string + - name: b + type: variant + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project + type: function +mixins: + snowpark_shared: + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project diff --git a/tests/test_data/projects/snowpark_function_secrets_without_external_access_v2/snowflake_V1.yml b/tests/test_data/projects/snowpark_function_secrets_without_external_access_v2/snowflake_V1.yml new file mode 100644 index 0000000000..5d59ce8ae0 --- /dev/null +++ b/tests/test_data/projects/snowpark_function_secrets_without_external_access_v2/snowflake_V1.yml @@ -0,0 +1,17 @@ +definition_version: 1 +snowpark: + project_name: "my_snowpark_project" + stage_name: "dev_deployment" + src: "app.py" + functions: + - name: func1 + handler: "app.func1_handler" + signature: + - name: "a" + type: "string" + - name: "b" + type: "variant" + returns: string + secrets: + cred: "cred_name" + other: "other_name" diff --git a/tests/test_data/projects/snowpark_functions_v2/app.py b/tests/test_data/projects/snowpark_functions_v2/app.py new file mode 100644 index 0000000000..0c336bf90f --- /dev/null +++ b/tests/test_data/projects/snowpark_functions_v2/app.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import sys + + +def hello(name: str) -> str: + return f"Hello {name}!" + + +# For local debugging. Be aware you may need to type-convert arguments if +# you add input parameters +if __name__ == "__main__": + if len(sys.argv) > 1: + print(hello(sys.argv[1])) # type: ignore + else: + print(hello("world")) diff --git a/tests/test_data/projects/snowpark_functions_v2/requirements.txt b/tests/test_data/projects/snowpark_functions_v2/requirements.txt new file mode 100644 index 0000000000..ed706cf032 --- /dev/null +++ b/tests/test_data/projects/snowpark_functions_v2/requirements.txt @@ -0,0 +1 @@ +snowflake-snowpark-python diff --git a/tests/test_data/projects/snowpark_functions_v2/snowflake.yml b/tests/test_data/projects/snowpark_functions_v2/snowflake.yml new file mode 100644 index 0000000000..2128f8cd91 --- /dev/null +++ b/tests/test_data/projects/snowpark_functions_v2/snowflake.yml @@ -0,0 +1,31 @@ +definition_version: '2' +entities: + func1: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: func1 + handler: app.func1_handler + returns: string + signature: + - name: a + type: string + default: default value + - name: b + type: variant + runtime: '3.10' + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project + type: function +mixins: + snowpark_shared: + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project diff --git a/tests/test_data/projects/snowpark_procedure_external_access_v2/app.py b/tests/test_data/projects/snowpark_procedure_external_access_v2/app.py new file mode 100644 index 0000000000..cf8e7ed1ac --- /dev/null +++ b/tests/test_data/projects/snowpark_procedure_external_access_v2/app.py @@ -0,0 +1,5 @@ +from snowflake.snowpark import Session + + +def hello(session: Session, name: str) -> str: + return f"Hello {name}" diff --git a/tests/test_data/projects/snowpark_procedure_external_access_v2/requirements.txt b/tests/test_data/projects/snowpark_procedure_external_access_v2/requirements.txt new file mode 100644 index 0000000000..e07b4c8561 --- /dev/null +++ b/tests/test_data/projects/snowpark_procedure_external_access_v2/requirements.txt @@ -0,0 +1,2 @@ +snowflake-snowpark-python +#snowcli # for local development diff --git a/tests/test_data/projects/snowpark_procedure_external_access_v2/snowflake.yml b/tests/test_data/projects/snowpark_procedure_external_access_v2/snowflake.yml new file mode 100644 index 0000000000..5d6f73896d --- /dev/null +++ b/tests/test_data/projects/snowpark_procedure_external_access_v2/snowflake.yml @@ -0,0 +1,32 @@ +definition_version: '2' +entities: + procedureName: + imports: [] + external_access_integrations: + - external_1 + - external_2 + secrets: + cred: cred_name + other: other_name + meta: + use_mixins: + - snowpark_shared + identifier: + name: procedureName + handler: app.hello + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project + type: procedure + execute_as_caller: false +mixins: + snowpark_shared: + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project diff --git a/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/app.zip b/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/app.zip new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/app/app.py b/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/app/app.py new file mode 100644 index 0000000000..8f932a90e7 --- /dev/null +++ b/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/app/app.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from snowflake.snowpark import Session + + +def hello_procedure(session: Session, name: str) -> str: + return f"Hello {name}" diff --git a/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/requirements.txt b/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/requirements.txt new file mode 100644 index 0000000000..e07b4c8561 --- /dev/null +++ b/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/requirements.txt @@ -0,0 +1,2 @@ +snowflake-snowpark-python +#snowcli # for local development diff --git a/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/snowflake.yml b/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/snowflake.yml new file mode 100644 index 0000000000..e47895cffb --- /dev/null +++ b/tests/test_data/projects/snowpark_procedure_fully_qualified_name_v2/snowflake.yml @@ -0,0 +1,133 @@ +definition_version: '2' +entities: + custom_db.custom_schema.fqn_procedure: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: custom_db.custom_schema.fqn_procedure + handler: app.hello_procedure + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: procedure + execute_as_caller: false + custom_schema.fqn_procedure_only_schema: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: custom_schema.fqn_procedure_only_schema + handler: app.hello_procedure + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: procedure + execute_as_caller: false + schema_procedure: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: schema_procedure + schema: custom_schema + handler: app.hello_procedure + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: procedure + execute_as_caller: false + database_procedure: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: database_procedure + database: custom_db + handler: app.hello_procedure + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: procedure + execute_as_caller: false + custom_schema.database_procedure: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: custom_schema.database_procedure + database: custom_db + handler: app.hello_procedure + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: procedure + execute_as_caller: false + custom_database_custom_schema_fqn_procedure_error: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: custom_database.custom_schema.fqn_procedure_error + schema: custom_schema + database: custom_database + handler: app.hello_procedure + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app + dest: my_snowpark_project + type: procedure + execute_as_caller: false +mixins: + snowpark_shared: + stage: dev_deployment + artifacts: + - src: app/ + dest: my_snowpark_project diff --git a/tests/test_data/projects/snowpark_procedure_secrets_without_external_access_v2/app.py b/tests/test_data/projects/snowpark_procedure_secrets_without_external_access_v2/app.py new file mode 100644 index 0000000000..cf8e7ed1ac --- /dev/null +++ b/tests/test_data/projects/snowpark_procedure_secrets_without_external_access_v2/app.py @@ -0,0 +1,5 @@ +from snowflake.snowpark import Session + + +def hello(session: Session, name: str) -> str: + return f"Hello {name}" diff --git a/tests/test_data/projects/snowpark_procedure_secrets_without_external_access_v2/app.zip b/tests/test_data/projects/snowpark_procedure_secrets_without_external_access_v2/app.zip new file mode 100644 index 0000000000..2dc9885eb1 Binary files /dev/null and b/tests/test_data/projects/snowpark_procedure_secrets_without_external_access_v2/app.zip differ diff --git a/tests/test_data/projects/snowpark_procedure_secrets_without_external_access_v2/snowflake.yml b/tests/test_data/projects/snowpark_procedure_secrets_without_external_access_v2/snowflake.yml new file mode 100644 index 0000000000..e5373f6919 --- /dev/null +++ b/tests/test_data/projects/snowpark_procedure_secrets_without_external_access_v2/snowflake.yml @@ -0,0 +1,30 @@ +definition_version: '2' +entities: + procedureName: + imports: [] + external_access_integrations: [] + secrets: + cred: cred_name + other: other_name + meta: + use_mixins: + - snowpark_shared + identifier: + name: procedureName + handler: app.hello + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project + type: procedure + execute_as_caller: false +mixins: + snowpark_shared: + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project diff --git a/tests/test_data/projects/snowpark_procedures_v2/app.py b/tests/test_data/projects/snowpark_procedures_v2/app.py new file mode 100644 index 0000000000..602af440e0 --- /dev/null +++ b/tests/test_data/projects/snowpark_procedures_v2/app.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import sys + +from snowflake.snowpark import Session + + +def hello(session: Session, name: str) -> str: + return f"Hello {name}" + + +def test(session: Session) -> str: + return "Test procedure" + + +# For local debugging. Be aware you may need to type-convert arguments if +# you add input parameters +if __name__ == "__main__": + from snowflake.cli.api.config import cli_config + + session = Session.builder.configs(cli_config.get_connection_dict("dev")).create() + if len(sys.argv) > 1: + print(hello(session, *sys.argv[1:])) # type: ignore + else: + print(hello(session)) # type: ignore + session.close() diff --git a/tests/test_data/projects/snowpark_procedures_v2/requirements.txt b/tests/test_data/projects/snowpark_procedures_v2/requirements.txt new file mode 100644 index 0000000000..e07b4c8561 --- /dev/null +++ b/tests/test_data/projects/snowpark_procedures_v2/requirements.txt @@ -0,0 +1,2 @@ +snowflake-snowpark-python +#snowcli # for local development diff --git a/tests/test_data/projects/snowpark_procedures_v2/snowflake.yml b/tests/test_data/projects/snowpark_procedures_v2/snowflake.yml new file mode 100644 index 0000000000..0eba1dccbd --- /dev/null +++ b/tests/test_data/projects/snowpark_procedures_v2/snowflake.yml @@ -0,0 +1,47 @@ +definition_version: '2' +entities: + procedureName: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: procedureName + handler: hello + returns: string + signature: + - name: name + type: string + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project + type: procedure + execute_as_caller: false + test: + imports: [] + external_access_integrations: [] + secrets: {} + meta: + use_mixins: + - snowpark_shared + identifier: + name: test + handler: test + returns: string + signature: '' + runtime: '3.10' + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project + type: procedure + execute_as_caller: false +mixins: + snowpark_shared: + stage: dev_deployment + artifacts: + - src: app.py + dest: my_snowpark_project diff --git a/tests/test_data/projects/streamlit_full_definition_v2/environment.yml b/tests/test_data/projects/streamlit_full_definition_v2/environment.yml new file mode 100644 index 0000000000..ac8feac3e8 --- /dev/null +++ b/tests/test_data/projects/streamlit_full_definition_v2/environment.yml @@ -0,0 +1,5 @@ +name: sf_env +channels: + - snowflake +dependencies: + - pandas diff --git a/tests/test_data/projects/streamlit_full_definition_v2/extra_file.py b/tests/test_data/projects/streamlit_full_definition_v2/extra_file.py new file mode 100644 index 0000000000..c84a9b135a --- /dev/null +++ b/tests/test_data/projects/streamlit_full_definition_v2/extra_file.py @@ -0,0 +1 @@ +foo = 42 diff --git a/tests/test_data/projects/streamlit_full_definition_v2/pages/my_page.py b/tests/test_data/projects/streamlit_full_definition_v2/pages/my_page.py new file mode 100644 index 0000000000..bc3ecbccba --- /dev/null +++ b/tests/test_data/projects/streamlit_full_definition_v2/pages/my_page.py @@ -0,0 +1,3 @@ +import streamlit as st + +st.title("Example page") diff --git a/tests/test_data/projects/streamlit_full_definition_v2/snowflake.yml b/tests/test_data/projects/streamlit_full_definition_v2/snowflake.yml new file mode 100644 index 0000000000..af40da7e2a --- /dev/null +++ b/tests/test_data/projects/streamlit_full_definition_v2/snowflake.yml @@ -0,0 +1,16 @@ +definition_version: '2' +entities: + test_streamlit: + identifier: + name: test_streamlit + type: streamlit + query_warehouse: test_warehouse + main_file: streamlit_app.py + pages_dir: pages + stage: streamlit + artifacts: + - streamlit_app.py + - environment.yml + - pages + - utils/utils.py + - extra_file.py diff --git a/tests/test_data/projects/streamlit_full_definition_v2/streamlit_app.py b/tests/test_data/projects/streamlit_full_definition_v2/streamlit_app.py new file mode 100644 index 0000000000..b699538d7e --- /dev/null +++ b/tests/test_data/projects/streamlit_full_definition_v2/streamlit_app.py @@ -0,0 +1,3 @@ +import streamlit as st + +st.title("Example streamlit app") diff --git a/tests/test_data/projects/streamlit_full_definition_v2/utils/utils.py b/tests/test_data/projects/streamlit_full_definition_v2/utils/utils.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_utils.py b/tests/test_utils.py index 70e746dd14..10eadc479a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -15,7 +15,6 @@ import json import os from pathlib import Path -from textwrap import dedent from unittest import mock from unittest.mock import MagicMock, patch @@ -37,7 +36,6 @@ from snowflake.cli.api.secure_path import SecurePath from snowflake.cli.api.utils import path_utils from snowflake.connector import SnowflakeConnection -from snowflake.connector.cursor import DictCursor from tests.test_data import test_data @@ -293,109 +291,153 @@ def test_get_host_region(host, expected): assert get_host_region(host) == expected -expected_ui_params_query = dedent( - f""" - select value['value']::string as PARAM_VALUE, value['name']::string as PARAM_NAME from table(flatten( - input => parse_json(SYSTEM$BOOTSTRAP_DATA_REQUEST()), - path => 'clientParamsInfo' - )) where value['name'] in ('ENABLE_EVENT_SHARING_V2_IN_THE_SAME_ACCOUNT', 'ENFORCE_MANDATORY_FILTERS_FOR_SAME_ACCOUNT_INSTALLATION', 'UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT'); - """ -) +expected_ui_params_query = "call system$bootstrap_data_request('CLIENT_PARAMS_INFO')" -def test_get_ui_parameters_no_param(): +def test_get_ui_parameters_no_param(mock_cursor): connection = MagicMock() - cursor = MagicMock() - connection.execute_string.return_value = (None, cursor) - cursor.fetchall.return_value = [] + connection.execute_string.return_value = ( + None, + mock_cursor([('{"clientParamsInfo": []}',)], []), + ) + assert get_ui_parameters(connection) == {} - connection.execute_string.assert_called_with( - expected_ui_params_query, cursor_class=DictCursor - ) + connection.execute_string.assert_called_with(expected_ui_params_query) -def test_get_ui_parameters_one_param(): +def test_get_ui_parameters_one_param(mock_cursor): connection = MagicMock() - cursor = MagicMock() - connection.execute_string.return_value = (None, cursor) - cursor.fetchall.return_value = [ - { - "PARAM_NAME": UIParameter.NA_ENABLE_REGIONLESS_REDIRECT.value, - "PARAM_VALUE": "true", - } - ] + connection.execute_string.return_value = ( + None, + mock_cursor( + [ + ( + """\ + { + "clientParamsInfo": [{ + "name": "UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT", + "value": true + }] + } + """, + ) + ], + [], + ), + ) + assert get_ui_parameters(connection) == { - UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "true" + UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: True } - connection.execute_string.assert_called_with( - expected_ui_params_query, cursor_class=DictCursor - ) + connection.execute_string.assert_called_with(expected_ui_params_query) -def test_get_ui_parameters_multiple_params(): +def test_get_ui_parameters_multiple_params(mock_cursor): connection = MagicMock() - cursor = MagicMock() - connection.execute_string.return_value = (None, cursor) - cursor.fetchall.return_value = [ - { - "PARAM_NAME": UIParameter.NA_ENABLE_REGIONLESS_REDIRECT.value, - "PARAM_VALUE": "true", - }, - { - "PARAM_NAME": UIParameter.NA_EVENT_SHARING_V2.value, - "PARAM_VALUE": "false", - }, - ] + connection.execute_string.return_value = ( + None, + mock_cursor( + [ + ( + """\ + { + "clientParamsInfo": [{ + "name": "UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT", + "value": true + }, + { + "name": "ENABLE_EVENT_SHARING_V2_IN_THE_SAME_ACCOUNT", + "value": false + }] + } + """, + ) + ], + [], + ), + ) + assert get_ui_parameters(connection) == { - UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: "true", - UIParameter.NA_EVENT_SHARING_V2: "false", + UIParameter.NA_ENABLE_REGIONLESS_REDIRECT: True, + UIParameter.NA_EVENT_SHARING_V2: False, } - connection.execute_string.assert_called_with( - expected_ui_params_query, cursor_class=DictCursor - ) + connection.execute_string.assert_called_with(expected_ui_params_query) -def test_get_ui_parameter_with_value(): +def test_get_ui_parameter_with_value(mock_cursor): connection = MagicMock() - cursor = MagicMock() - connection.execute_string.return_value = (None, cursor) - cursor.fetchall.return_value = [ - { - "PARAM_NAME": UIParameter.NA_ENABLE_REGIONLESS_REDIRECT.value, - "PARAM_VALUE": "true", - } - ] + connection.execute_string.return_value = ( + None, + mock_cursor( + [ + ( + """\ + { + "clientParamsInfo": [{ + "name": "UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT", + "value": true + }] + } + """, + ) + ], + [], + ), + ) assert ( - get_ui_parameter(connection, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, "false") - == "true" + get_ui_parameter(connection, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, False) + is True ) -def test_get_ui_parameter_with_empty_value_then_use_empty_value(): +def test_get_ui_parameter_with_empty_value_then_use_empty_value(mock_cursor): connection = MagicMock() - cursor = MagicMock() - connection.execute_string.return_value = (None, cursor) - cursor.fetchall.return_value = [ - { - "PARAM_NAME": UIParameter.NA_ENABLE_REGIONLESS_REDIRECT.value, - "PARAM_VALUE": "", - } - ] + connection.execute_string.return_value = ( + None, + mock_cursor( + [ + ( + """\ + { + "clientParamsInfo": [{ + "name": "UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT", + "value": "" + }] + } + """, + ) + ], + [], + ), + ) assert ( get_ui_parameter(connection, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, "false") == "" ) -def test_get_ui_parameter_with_no_value_then_use_default(): +def test_get_ui_parameter_with_no_value_then_use_default(mock_cursor): connection = MagicMock() - cursor = MagicMock() - connection.execute_string.return_value = (None, cursor) - cursor.fetchall.return_value = [] + connection.execute_string.return_value = ( + None, + mock_cursor( + [ + ( + """\ + { + "clientParamsInfo": [] + } + """, + ) + ], + [], + ), + ) + assert ( - get_ui_parameter(connection, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, "false") - == "false" + get_ui_parameter(connection, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, "any") + == "any" ) diff --git a/tests_e2e/conftest.py b/tests_e2e/conftest.py index 7bca4d9d1c..38bbb8327c 100644 --- a/tests_e2e/conftest.py +++ b/tests_e2e/conftest.py @@ -140,7 +140,7 @@ def _install_snowcli_with_external_plugin( ) # Required by snowpark example tests - _pip_install(python, "snowflake-snowpark-python") + _pip_install(python, "snowflake-snowpark-python[pandas]==1.25.0") def _python_path(venv_path: Path) -> Path: diff --git a/tests_integration/helpers/test_v1_to_v2.py b/tests_integration/helpers/test_v1_to_v2.py index bfc594ff0b..a4aae93244 100644 --- a/tests_integration/helpers/test_v1_to_v2.py +++ b/tests_integration/helpers/test_v1_to_v2.py @@ -2,7 +2,9 @@ import pytest -from snowflake.cli._plugins.nativeapp.codegen.compiler import TEMPLATES_PROCESSOR +from snowflake.cli._plugins.nativeapp.codegen.templates.templates_processor import ( + TemplatesProcessor, +) from tests.nativeapp.factories import ProjectV11Factory @@ -44,10 +46,10 @@ def test_v1_to_v2_converts_templates_in_files(temp_dir, runner): pdf__native_app__package__name="my_pkg", pdf__native_app__application__name="my_app", pdf__native_app__artifacts=[ - dict(src="templated.txt", processors=[TEMPLATES_PROCESSOR]), + dict(src="templated.txt", processors=[TemplatesProcessor.NAME]), dict(src="untemplated.txt"), - dict(src="app/*", processors=[TEMPLATES_PROCESSOR]), - dict(src="nested/*", processors=[TEMPLATES_PROCESSOR]), + dict(src="app/*", processors=[TemplatesProcessor.NAME]), + dict(src="nested/*", processors=[TemplatesProcessor.NAME]), ], files={ filename: source_contents diff --git a/tests_integration/nativeapp/__snapshots__/test_version.ambr b/tests_integration/nativeapp/__snapshots__/test_version.ambr index 8c0ac6e37a..fcc3c0c6bc 100644 --- a/tests_integration/nativeapp/__snapshots__/test_version.ambr +++ b/tests_integration/nativeapp/__snapshots__/test_version.ambr @@ -3,24 +3,18 @@ list([ dict({ 'comment': None, - 'dropped_on': None, 'label': '', - 'log_level': 'OFF', 'patch': 0, 'review_status': 'NOT_REVIEWED', 'state': 'READY', - 'trace_level': 'OFF', 'version': 'V1', }), dict({ 'comment': None, - 'dropped_on': None, 'label': '', - 'log_level': 'OFF', 'patch': 1, 'review_status': 'NOT_REVIEWED', 'state': 'READY', - 'trace_level': 'OFF', 'version': 'V1', }), ]) diff --git a/tests_integration/nativeapp/test_teardown.py b/tests_integration/nativeapp/test_teardown.py index 44c94a8389..03d11a6e2b 100644 --- a/tests_integration/nativeapp/test_teardown.py +++ b/tests_integration/nativeapp/test_teardown.py @@ -261,6 +261,7 @@ def test_nativeapp_teardown_pkg_versions( assert result.exit_code == 0 +@pytest.mark.integration def test_nativeapp_teardown_multiple_apps_using_snow_app( runner, nativeapp_project_directory, @@ -322,6 +323,7 @@ def test_nativeapp_teardown_multiple_apps_using_snow_app( ) +@pytest.mark.integration def test_nativeapp_teardown_multiple_packages_using_snow_app_must_choose( runner, nativeapp_project_directory, diff --git a/tests_integration/nativeapp/test_version.py b/tests_integration/nativeapp/test_version.py index ce9706afba..701f9e9fc0 100644 --- a/tests_integration/nativeapp/test_version.py +++ b/tests_integration/nativeapp/test_version.py @@ -31,6 +31,17 @@ from tests_integration.test_utils import contains_row_with, row_from_snowflake_session +# A minimal set of fields to compare when checking version output +VERSION_FIELDS_TO_OUTPUT = [ + "comment", + "label", + "patch", + "review_status", + "state", + "version", +] + + def set_version_in_app_manifest(manifest_path: Path, version: Any, patch: Any = None): with open(manifest_path, "r") as f: manifest = safe_load(f) @@ -373,10 +384,10 @@ def test_nativeapp_version_create_package_no_magic_comment( # app package contains version v1 with 2 patches actual = runner.invoke_with_connection_json(split(list_command)) for row in actual.json: - # Remove date field - row.pop("created_on", None) - # Remove metric_level field - row.pop("metric_level", None) + keys_to_remove = row.keys() - VERSION_FIELDS_TO_OUTPUT + for key in keys_to_remove: + del row[key] + assert actual.json == snapshot diff --git a/tests_integration/test_object.py b/tests_integration/test_object.py index a7d2a57bee..386150438b 100644 --- a/tests_integration/test_object.py +++ b/tests_integration/test_object.py @@ -349,7 +349,7 @@ def test_create_error_undefined_database(runner): ) -@pytest.mark.int +@pytest.mark.integration def test_object_create_if_not_exist_and_replace(runner, test_database): result = runner.invoke_with_connection(