From 2bf2e635908b3178ce4b135137ac24d3ca1768c8 Mon Sep 17 00:00:00 2001 From: Michel El Nacouzi Date: Tue, 7 Jan 2025 14:52:07 -0500 Subject: [PATCH] Add release channel publish command --- RELEASE-NOTES.md | 1 + .../cli/_plugins/nativeapp/commands.py | 48 ++ .../cli/_plugins/nativeapp/constants.py | 1 + .../nativeapp/entities/application_package.py | 201 ++++++- .../cli/_plugins/nativeapp/sf_sql_facade.py | 43 ++ .../_plugins/nativeapp/version/commands.py | 4 +- .../cli/_plugins/workspace/commands.py | 4 +- src/snowflake/cli/api/entities/common.py | 2 + tests/__snapshots__/test_help_messages.ambr | 170 ++++++ .../test_application_package_entity.py | 527 +++++++++++++++++- tests/nativeapp/test_sf_sql_facade.py | 189 +++++-- tests/nativeapp/utils.py | 1 + 12 files changed, 1091 insertions(+), 100 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 2763cc0a24..8502130953 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -31,6 +31,7 @@ * Add ability to list release channels through `snow app release-channel list` command * Add ability to add and remove accounts from release channels through `snow app release-channel add-accounts` and snow app release-channel remove-accounts` commands. * Add ability to add/remove versions to/from release channels through `snow app release-channel add-version` and `snow app release-channel remove-version` commands. +* Add publish command to make it easier to manage publishing versions to release channels and updating release directives: `snow app publish` ## Fixes and improvements * Fixed crashes with older x86_64 Intel CPUs. diff --git a/src/snowflake/cli/_plugins/nativeapp/commands.py b/src/snowflake/cli/_plugins/nativeapp/commands.py index d33c0d2018..af35acc208 100644 --- a/src/snowflake/cli/_plugins/nativeapp/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/commands.py @@ -532,3 +532,51 @@ def message(self): @property def result(self): return self._element + + +@app.command("publish", requires_connection=True) +@with_project_definition() +@force_project_definition_v2() +def app_publish( + version: str = typer.Option( + show_default=False, + help="The version to publish to the release channel. The version must be created fist using 'snow app version create'.", + ), + patch: int = typer.Option( + show_default=False, + help="The patch number under the given version. The patch number must be created first using 'snow app version create'.", + ), + channel: Optional[str] = typer.Option( + "DEFAULT", + help="The name of the release channel to publish to. If not provided, the default release channel is used.", + ), + directive: Optional[str] = typer.Option( + "DEFAULT", + help="The name of the release directive to update with the specified version and patch. If not provided, the default release directive is used.", + ), + interactive: bool = InteractiveOption, + force: Optional[bool] = ForceOption, + **options, +) -> CommandResult: + """ + Adds the version to the release channel and updates the release directive with the new version and patch. + """ + cli_context = get_cli_context() + ws = WorkspaceManager( + project_definition=cli_context.project_definition, + project_root=cli_context.project_root, + ) + package_id = options["package_entity_id"] + ws.perform_action( + package_id, + EntityActions.PUBLISH, + version=version, + patch=patch, + release_channel=channel, + release_directive=directive, + interactive=interactive, + force=force, + ) + return MessageResult( + f"Version {version} and patch {patch} published to release directive {directive} of release channel {channel}." + ) diff --git a/src/snowflake/cli/_plugins/nativeapp/constants.py b/src/snowflake/cli/_plugins/nativeapp/constants.py index 829e2751f9..4d8cb80d14 100644 --- a/src/snowflake/cli/_plugins/nativeapp/constants.py +++ b/src/snowflake/cli/_plugins/nativeapp/constants.py @@ -30,3 +30,4 @@ DEFAULT_CHANNEL = "DEFAULT" DEFAULT_DIRECTIVE = "DEFAULT" +MAX_VERSIONS_IN_RELEASE_CHANNEL = 2 diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py index 1da13ca9d5..78a23567bb 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py @@ -3,6 +3,7 @@ import json import os import re +from datetime import datetime from pathlib import Path from textwrap import dedent from typing import Any, List, Literal, Optional, Set, Union @@ -27,6 +28,7 @@ DEFAULT_DIRECTIVE, EXTERNAL_DISTRIBUTION, INTERNAL_DISTRIBUTION, + MAX_VERSIONS_IN_RELEASE_CHANNEL, NAME_COL, OWNER_COL, PATCH_COL, @@ -53,7 +55,7 @@ from snowflake.cli._plugins.nativeapp.sf_facade_exceptions import ( InsufficientPrivilegesError, ) -from snowflake.cli._plugins.nativeapp.sf_sql_facade import ReleaseChannel +from snowflake.cli._plugins.nativeapp.sf_sql_facade import ReleaseChannel, Version from snowflake.cli._plugins.nativeapp.utils import needs_confirmation, sanitize_dir_name from snowflake.cli._plugins.snowpark.snowpark_entity_model import ( FunctionEntityModel, @@ -333,21 +335,14 @@ def action_drop(self, action_ctx: ActionContext, force_drop: bool, *args, **kwar ) return - with sql_executor.use_role(self.role): - # 2. Check for versions in the application package - show_versions_query = f"show versions in application package {self.name}" - show_versions_cursor = sql_executor.execute_query( - show_versions_query, cursor_class=DictCursor - ) - if show_versions_cursor.rowcount is None: - raise SnowflakeSQLExecutionError(show_versions_query) - - if show_versions_cursor.rowcount > 0: - # allow dropping a package with versions when --force is set - if not force_drop: - raise CouldNotDropApplicationPackageWithVersions( - "Drop versions first, or use --force to override." - ) + # 2. Check for versions in the application package + versions_in_pkg = get_snowflake_facade().show_versions(self.name, self.role) + if len(versions_in_pkg) > 0: + # allow dropping a package with versions when --force is set + if not force_drop: + raise CouldNotDropApplicationPackageWithVersions( + "Drop versions first, or use --force to override." + ) # 3. Check distribution of the existing application package actual_distribution = self.get_app_pkg_distribution_in_snowflake() @@ -422,15 +417,7 @@ def action_version_list( Get all existing versions, if defined, for an application package. It executes a 'show versions in application package' query and returns all the results. """ - sql_executor = get_sql_executor() - with sql_executor.use_role(self.role): - show_obj_query = f"show versions in application package {self.name}" - show_obj_cursor = sql_executor.execute_query(show_obj_query) - - if show_obj_cursor.rowcount is None: - raise SnowflakeSQLExecutionError(show_obj_query) - - return show_obj_cursor + return get_snowflake_facade().show_versions(self.name, self.role) def action_version_create( self, @@ -634,7 +621,9 @@ def _validate_target_accounts(self, accounts: list[str]) -> None: ) def get_sanitized_release_channel( - self, release_channel: Optional[str] + self, + release_channel: Optional[str], + available_release_channels: Optional[list[ReleaseChannel]] = None, ) -> Optional[str]: """ Sanitize the release channel name provided by the user and validate it against the available release channels. @@ -646,9 +635,10 @@ def get_sanitized_release_channel( if not release_channel: return None - available_release_channels = get_snowflake_facade().show_release_channels( - self.name, self.role - ) + if available_release_channels is None: + available_release_channels = get_snowflake_facade().show_release_channels( + self.name, self.role + ) if not available_release_channels and same_identifiers( release_channel, DEFAULT_CHANNEL @@ -970,6 +960,159 @@ def action_release_channel_remove_version( role=self.role, ) + def _find_version_with_no_recent_update( + self, versions_info: list[Version], free_versions: set[str] + ) -> Optional[str]: + """ + Finds the version with the oldest created_on date from the free versions. + """ + + if not free_versions: + return None + + # map of versionId to last Updated Date. Last Updated Date is based on patch creation date. + last_updated_map: dict[str, datetime] = {} + for version_info in versions_info: + last_updated_value = last_updated_map.get(version_info["version"], None) + if ( + not last_updated_value + or version_info["created_on"] > last_updated_value + ): + last_updated_map[version_info["version"]] = version_info["created_on"] + + oldest_version = None + oldest_version_last_updated_on = None + + for version in free_versions: + last_updated = last_updated_map[version] + if not oldest_version or last_updated < oldest_version_last_updated_on: + oldest_version = version + oldest_version_last_updated_on = last_updated + + return oldest_version + + def action_publish( + self, + action_ctx: ActionContext, + version: str, + patch: int, + release_channel: Optional[str], + release_directive: str, + interactive: bool, + force: bool, + *args, + **kwargs, + ) -> None: + """ + Publishes a version and a patch to a release directive of a release channel. + + The version is first added to the release channel, + and then the release directive is set to the version and patch provided. + + If the number of versions in a release channel exceeds the maximum allowable versions, + the user is prompted to remove an existing version to make space for the new version. + """ + if force: + policy = AllowAlwaysPolicy() + elif interactive: + policy = AskAlwaysPolicy() + else: + policy = DenyAlwaysPolicy() + + console = self._workspace_ctx.console + versions_info = get_snowflake_facade().show_versions(self.name, self.role) + + available_patches = [ + version_info["patch"] + for version_info in versions_info + if version_info["version"] == unquote_identifier(version) + ] + + if not available_patches: + raise UsageError( + f"Version {version} does not exist in application package {self.name}." + ) + + if patch not in available_patches: + raise UsageError( + f"Patch {patch} does not exist for version {version} in application package {self.name}." + ) + + available_release_channels = get_snowflake_facade().show_release_channels( + self.name, self.role + ) + + release_channel = self.get_sanitized_release_channel( + release_channel, available_release_channels + ) + + if release_channel: + release_channel_info = {} + for channel_info in available_release_channels: + if channel_info["name"] == unquote_identifier(release_channel): + release_channel_info = channel_info + break + + versions_in_channel = release_channel_info["versions"] + if unquote_identifier(version) not in release_channel_info["versions"]: + if len(versions_in_channel) >= MAX_VERSIONS_IN_RELEASE_CHANNEL: + # If we hit the maximum allowable versions in a release channel, we need to remove one version to make space for the new version + all_release_directives = ( + get_snowflake_facade().show_release_directives( + package_name=self.name, + role=self.role, + release_channel=release_channel, + ) + ) + + # check which versions are attached to any release directive + targeted_versions = {d["version"] for d in all_release_directives} + + free_versions = { + v for v in versions_in_channel if v not in targeted_versions + } + + if not free_versions: + raise UsageError( + f"Maximum number of versions in release channel {release_channel} reached. Cannot add more versions." + ) + + version_to_remove = self._find_version_with_no_recent_update( + versions_info, free_versions + ) + user_prompt = f"Maximum number of versions in release channel reached. Would you like to remove version {version_to_remove} to make space for version {version}?" + if not policy.should_proceed(user_prompt): + raise UsageError( + "Cannot proceed with publishing the new version. Please remove an existing version from the release channel to make space for the new version, or use --force to automatically clean up unused versions." + ) + + console.warning( + f"Maximum number of versions in release channel reached. Removing version {version_to_remove} from release_channel {release_channel} to make space for version {version}." + ) + get_snowflake_facade().remove_version_from_release_channel( + package_name=self.name, + release_channel=release_channel, + version=version_to_remove, + role=self.role, + ) + + get_snowflake_facade().add_version_to_release_channel( + package_name=self.name, + release_channel=release_channel, + version=version, + role=self.role, + ) + + get_snowflake_facade().set_release_directive( + package_name=self.name, + release_directive=release_directive, + release_channel=release_channel, + target_accounts=None, + version=version, + patch=patch, + role=self.role, + ) + def _bundle_children(self, action_ctx: ActionContext) -> List[str]: # Create _children directory children_artifacts_dir = self.children_artifacts_deploy_root diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index 8ad6e01a9c..4c92c8ccea 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -97,6 +97,17 @@ }, ) +Version = TypedDict( + "Version", + { + "version": str, + "patch": int, + "label": str | None, + "created_on": datetime, + "review_status": str, + }, +) + class SnowflakeSQLFacade: def __init__(self, sql_executor: BaseSqlExecutor | None = None): @@ -1413,6 +1424,38 @@ def remove_version_from_release_channel( f"Failed to remove version {version} from release channel {release_channel} in application package {package_name}.", ) + def show_versions( + self, + package_name: str, + role: str | None = None, + ) -> list[Version]: + """ + Show all versions in an application package. + + @param package_name: Name of the application package + @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) + + with self._use_role_optional(role): + try: + cursor = self._sql_executor.execute_query( + f"show versions in application package {package_name}", + cursor_class=DictCursor, + ) + except Exception as err: + if isinstance(err, ProgrammingError): + if err.errno == DOES_NOT_EXIST_OR_NOT_AUTHORIZED: + raise UserInputError( + f"Application package {package_name} does not exist or you are not authorized to access it." + ) from err + handle_unclassified_error( + err, + f"Failed to show versions for application package {package_name}.", + ) + + return cursor.fetchall() + def _strip_empty_lines(text: str) -> str: """ diff --git a/src/snowflake/cli/_plugins/nativeapp/version/commands.py b/src/snowflake/cli/_plugins/nativeapp/version/commands.py index b7ad13c0c0..314a051c73 100644 --- a/src/snowflake/cli/_plugins/nativeapp/version/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/version/commands.py @@ -32,10 +32,10 @@ from snowflake.cli.api.entities.common import EntityActions from snowflake.cli.api.output.formats import OutputFormat from snowflake.cli.api.output.types import ( + CollectionResult, CommandResult, MessageResult, ObjectResult, - QueryResult, ) from snowflake.cli.api.project.util import to_identifier @@ -130,7 +130,7 @@ def version_list( package_id, EntityActions.VERSION_LIST, ) - return QueryResult(cursor) + return CollectionResult(cursor) @app.command(requires_connection=True) diff --git a/src/snowflake/cli/_plugins/workspace/commands.py b/src/snowflake/cli/_plugins/workspace/commands.py index e09078df45..dc4db61fbe 100644 --- a/src/snowflake/cli/_plugins/workspace/commands.py +++ b/src/snowflake/cli/_plugins/workspace/commands.py @@ -34,7 +34,7 @@ from snowflake.cli.api.commands.snow_typer import SnowTyperFactory from snowflake.cli.api.entities.common import EntityActions from snowflake.cli.api.exceptions import IncompatibleParametersError -from snowflake.cli.api.output.types import MessageResult, QueryResult +from snowflake.cli.api.output.types import CollectionResult, MessageResult ws = SnowTyperFactory( name="ws", @@ -243,7 +243,7 @@ def version_list( entity_id, EntityActions.VERSION_LIST, ) - return QueryResult(cursor) + return CollectionResult(cursor) @version.command(name="create", requires_connection=True, hidden=True) diff --git a/src/snowflake/cli/api/entities/common.py b/src/snowflake/cli/api/entities/common.py index cbedb87825..384aad2661 100644 --- a/src/snowflake/cli/api/entities/common.py +++ b/src/snowflake/cli/api/entities/common.py @@ -27,6 +27,8 @@ class EntityActions(str, Enum): RELEASE_CHANNEL_ADD_VERSION = "action_release_channel_add_version" RELEASE_CHANNEL_REMOVE_VERSION = "action_release_channel_remove_version" + PUBLISH = "action_publish" + T = TypeVar("T") diff --git a/tests/__snapshots__/test_help_messages.ambr b/tests/__snapshots__/test_help_messages.ambr index 9e8d9b808d..f3cda791f3 100644 --- a/tests/__snapshots__/test_help_messages.ambr +++ b/tests/__snapshots__/test_help_messages.ambr @@ -578,6 +578,172 @@ +------------------------------------------------------------------------------+ + ''' +# --- +# name: test_help_messages[app.publish] + ''' + + Usage: default app publish [OPTIONS] + + Adds the version to the release channel and updates the release directive with + the new version and patch. + + +- Options --------------------------------------------------------------------+ + | * --version TEXT The version to | + | publish to the | + | release channel. | + | The version must be | + | created fist using | + | 'snow app version | + | create'. | + | [required] | + | * --patch INTEGER The patch number | + | under the given | + | version. The patch | + | number must be | + | created first using | + | 'snow app version | + | create'. | + | [required] | + | --channel TEXT The name of the | + | release channel to | + | publish to. If not | + | provided, the | + | default release | + | channel is used. | + | [default: DEFAULT] | + | --directive TEXT The name of the | + | release directive | + | to update with the | + | specified version | + | and patch. If not | + | provided, the | + | default release | + | directive is used. | + | [default: DEFAULT] | + | --interactive --no-interactive When enabled, this | + | option displays | + | prompts even if the | + | standard input and | + | output are not | + | terminal devices. | + | Defaults to True in | + | an interactive | + | shell environment, | + | and False | + | otherwise. | + | --force When enabled, this | + | option causes the | + | command to | + | implicitly approve | + | any prompts that | + | arise. You should | + | enable this option | + | if interactive mode | + | is not specified | + | and if you want | + | perform potentially | + | destructive | + | actions. Defaults | + | to unset. | + | --package-entity-id TEXT The ID of the | + | package entity on | + | which to operate | + | when | + | definition_version | + | is 2 or higher. | + | --app-entity-id TEXT The ID of the | + | application entity | + | on which to operate | + | when | + | definition_version | + | is 2 or higher. | + | --project -p TEXT Path where | + | Snowflake project | + | resides. Defaults | + | to current working | + | directory. | + | --env TEXT String in format of | + | key=value. | + | Overrides variables | + | from env section | + | used for templates. | + | --help -h Show this message | + | and exit. | + +------------------------------------------------------------------------------+ + +- Connection configuration ---------------------------------------------------+ + | --connection,--environment -c TEXT Name of the connection, as | + | defined in your config.toml | + | file. Default: default. | + | --host TEXT Host address for the | + | connection. Overrides the | + | value specified for the | + | connection. | + | --port INTEGER Port for the connection. | + | Overrides the value | + | specified for the | + | connection. | + | --account,--accountname TEXT Name assigned to your | + | Snowflake account. Overrides | + | the value specified for the | + | connection. | + | --user,--username TEXT Username to connect to | + | Snowflake. Overrides the | + | value specified for the | + | connection. | + | --password TEXT Snowflake password. | + | Overrides the value | + | specified for the | + | connection. | + | --authenticator TEXT Snowflake authenticator. | + | Overrides the value | + | specified for the | + | connection. | + | --private-key-file,--privateā€¦ TEXT Snowflake private key file | + | path. Overrides the value | + | specified for the | + | connection. | + | --token-file-path TEXT Path to file with an OAuth | + | token that should be used | + | when connecting to Snowflake | + | --database,--dbname TEXT Database to use. Overrides | + | the value specified for the | + | connection. | + | --schema,--schemaname TEXT Database schema to use. | + | Overrides the value | + | specified for the | + | connection. | + | --role,--rolename TEXT Role to use. Overrides the | + | value specified for the | + | connection. | + | --warehouse TEXT Warehouse to use. Overrides | + | the value specified for the | + | connection. | + | --temporary-connection -x Uses connection defined with | + | command line parameters, | + | instead of one defined in | + | config | + | --mfa-passcode TEXT Token to use for | + | multi-factor authentication | + | (MFA) | + | --enable-diag Run Python connector | + | diagnostic test | + | --diag-log-path TEXT Diagnostic report path | + | --diag-allowlist-path TEXT Diagnostic report path to | + | optional allowlist | + +------------------------------------------------------------------------------+ + +- Global configuration -------------------------------------------------------+ + | --format [TABLE|JSON] Specifies the output format. | + | [default: TABLE] | + | --verbose -v Displays log entries for log levels info | + | and higher. | + | --debug Displays log entries for log levels debug | + | and higher; debug logs contain additional | + | information. | + | --silent Turns off intermediate output to console. | + +------------------------------------------------------------------------------+ + + ''' # --- # name: test_help_messages[app.release-channel.add-accounts] @@ -2313,6 +2479,8 @@ | configured in Snowflake. | | open Opens the Snowflake Native App inside of your browser, | | once it has been installed in your account. | + | publish Adds the version to the release channel and updates the | + | release directive with the new version and patch. | | release-channel Manages release channels of an application package | | release-directive Manages release directives of an application package | | run Creates an application package in your Snowflake | @@ -10749,6 +10917,8 @@ | configured in Snowflake. | | open Opens the Snowflake Native App inside of your browser, | | once it has been installed in your account. | + | publish Adds the version to the release channel and updates the | + | release directive with the new version and patch. | | release-channel Manages release channels of an application package | | release-directive Manages release directives of an application package | | run Creates an application package in your Snowflake | diff --git a/tests/nativeapp/test_application_package_entity.py b/tests/nativeapp/test_application_package_entity.py index a472da7d56..fdadd0d639 100644 --- a/tests/nativeapp/test_application_package_entity.py +++ b/tests/nativeapp/test_application_package_entity.py @@ -47,6 +47,7 @@ SQL_FACADE_SET_RELEASE_DIRECTIVE, SQL_FACADE_SHOW_RELEASE_CHANNELS, SQL_FACADE_SHOW_RELEASE_DIRECTIVES, + SQL_FACADE_SHOW_VERSIONS, SQL_FACADE_UNSET_RELEASE_DIRECTIVE, mock_execute_helper, ) @@ -188,29 +189,22 @@ def test_deploy( assert mock_execute.mock_calls == expected -@mock.patch(SQL_EXECUTOR_EXECUTE) -def test_version_list( - mock_execute, application_package_entity, action_context, mock_cursor -): +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +def test_version_list(show_versions, application_package_entity, action_context): pkg_model = application_package_entity._entity_model # noqa SLF001 pkg_model.meta.role = "package_role" - side_effects, expected = mock_execute_helper( - [ - ( - mock_cursor([("old_role",)], []), - mock.call("select current_role()"), - ), - (None, mock.call(f"use role {pkg_model.meta.role}")), - ( - mock_cursor([], []), - mock.call(f"show versions in application package {pkg_model.fqn.name}"), - ), - (None, mock.call("use role old_role")), - ] - ) - mock_execute.side_effect = side_effects - application_package_entity.action_version_list(action_context) - assert mock_execute.mock_calls == expected + + expected_versions = [ + {"version": "1.0", "patch": 1}, + {"version": "1.1", "patch": 2}, + ] + + show_versions.return_value = expected_versions + + result = application_package_entity.action_version_list(action_context) + assert result == expected_versions + + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) @mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS, return_value=[]) @@ -1417,3 +1411,494 @@ def test_given_invalid_release_channel_when_release_channel_remove_version_then_ ) remove_version_from_release_channel.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +def test_given_release_channel_and_version_when_publish_then_success( + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [{"name": "TEST_CHANNEL", "versions": []}] + show_release_directives.return_value = [{"name": "TEST_DIRECTIVE"}] + show_versions.return_value = [{"version": "1.0", "patch": 1}] + + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version="1.0", + patch=1, + interactive=False, + force=False, + ) + + show_release_channels.assert_called_once_with( + pkg_model.fqn.name, pkg_model.meta.role + ) + show_release_directives.assert_not_called() + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) + add_version_to_release_channel.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + release_channel="test_channel", + version="1.0", + ) + remove_version_from_release_channel.assert_not_called() + set_release_directive.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + version="1.0", + patch=1, + release_channel="test_channel", + release_directive="test_directive", + target_accounts=None, + ) + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +def test_given_release_channel_and_version_already_in_channel_when_publish_then_success( + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [{"name": "TEST_CHANNEL", "versions": ["1.0"]}] + show_release_directives.return_value = [{"name": "TEST_DIRECTIVE"}] + show_versions.return_value = [{"version": "1.0", "patch": 1}] + + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version="1.0", + patch=1, + interactive=False, + force=False, + ) + + show_release_channels.assert_called_once_with( + pkg_model.fqn.name, pkg_model.meta.role + ) + show_release_directives.assert_not_called() + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) + add_version_to_release_channel.assert_not_called() + remove_version_from_release_channel.assert_not_called() + set_release_directive.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + version="1.0", + patch=1, + release_channel="test_channel", + release_directive="test_directive", + target_accounts=None, + ) + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +def test_given_release_channels_disabled_when_publish_to_default_channel_then_success( + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [] + show_release_directives.return_value = [{"name": "TEST_DIRECTIVE"}] + show_versions.return_value = [{"version": "1.0", "patch": 1}] + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="default", + release_directive="test_directive", + version="1.0", + patch=1, + interactive=False, + force=False, + ) + + show_release_channels.assert_called_once_with( + pkg_model.fqn.name, pkg_model.meta.role + ) + show_release_directives.assert_not_called() + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) + add_version_to_release_channel.assert_not_called() + remove_version_from_release_channel.assert_not_called() + set_release_directive.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + version="1.0", + patch=1, + release_channel=None, + release_directive="test_directive", + target_accounts=None, + ) + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +def test_given_release_channels_disabled_when_publish_to_non_default_channel_then_error( + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [] + show_release_directives.return_value = [{"name": "TEST_DIRECTIVE"}] + show_versions.return_value = [{"version": "1.0", "patch": 1}] + with pytest.raises(UsageError) as e: + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="non_default", + release_directive="test_directive", + version="1.0", + patch=1, + interactive=False, + force=False, + ) + + assert ( + str(e.value) + == f"Release channels are not enabled for application package {pkg_model.fqn.name}." + ) + + show_release_channels.assert_called_once_with( + pkg_model.fqn.name, pkg_model.meta.role + ) + show_release_directives.assert_not_called() + add_version_to_release_channel.assert_not_called() + remove_version_from_release_channel.assert_not_called() + set_release_directive.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +def test_given_non_existing_version_when_publish_then_error( + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [{"name": "TEST_CHANNEL", "versions": []}] + show_release_directives.return_value = [{"name": "TEST_DIRECTIVE"}] + show_versions.return_value = [{"version": "1.3", "patch": 1}] + with pytest.raises(UsageError) as e: + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version="1.0", + patch=1, + interactive=False, + force=False, + ) + + assert ( + str(e.value) + == f"Version 1.0 does not exist in application package {pkg_model.fqn.name}." + ) + + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) + add_version_to_release_channel.assert_not_called() + remove_version_from_release_channel.assert_not_called() + set_release_directive.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +def test_given_non_existing_patch_when_publish_then_error( + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [{"name": "TEST_CHANNEL", "versions": []}] + show_release_directives.return_value = [{"name": "TEST_DIRECTIVE"}] + show_versions.return_value = [{"version": "1.0", "patch": 2}] + + with pytest.raises(UsageError) as e: + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version="1.0", + patch=1, + interactive=False, + force=False, + ) + + assert ( + str(e.value) + == f"Patch 1 does not exist for version 1.0 in application package {pkg_model.fqn.name}." + ) + + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) + add_version_to_release_channel.assert_not_called() + remove_version_from_release_channel.assert_not_called() + set_release_directive.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +def test_given_versions_referenced_by_existing_release_directives_when_publish_then_error( + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [ + {"name": "TEST_CHANNEL", "versions": ["1.0", "2.0"]} + ] + show_release_directives.return_value = [ + {"name": "TEST_DIRECTIVE", "version": "1.0", "patch": 1}, + {"name": "TEST_DIRECTIVE2", "version": "2.0", "patch": 1}, + ] + show_versions.return_value = [ + {"version": "1.0", "patch": 1}, + {"version": "2.0", "patch": 1}, + {"version": "3.0", "patch": 2}, + ] + with pytest.raises(UsageError) as e: + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version="3.0", + patch=2, + interactive=False, + force=False, + ) + + assert ( + str(e.value) + == "Maximum number of versions in release channel test_channel reached. Cannot add more versions." + ) + + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) + show_release_directives.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + release_channel="test_channel", + ) + add_version_to_release_channel.assert_not_called() + remove_version_from_release_channel.assert_not_called() + set_release_directive.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +def test_given_only_one_version_referenced_by_existing_release_directive_when_publish_then_remove_unused_version( + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + mock_console = mock.MagicMock() + application_package_entity._workspace_ctx.console = mock_console # noqa SLF001 + + show_release_channels.return_value = [ + {"name": "TEST_CHANNEL", "versions": ["1.0", "2.0"]} + ] + show_release_directives.return_value = [ + {"name": "TEST_DIRECTIVE", "version": "1.0", "patch": 1} + ] + show_versions.return_value = [ + {"version": "1.0", "patch": 1, "created_on": datetime(2024, 12, 3)}, + {"version": "2.0", "patch": 1, "created_on": datetime(2024, 12, 5)}, + {"version": "3.0", "patch": 2, "created_on": datetime(2024, 12, 6)}, + ] + + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version="3.0", + patch=2, + interactive=False, + force=True, + ) + + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) + show_release_directives.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + release_channel="test_channel", + ) + show_release_channels.assert_called_once_with( + pkg_model.fqn.name, pkg_model.meta.role + ) + remove_version_from_release_channel.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + release_channel="test_channel", + version="2.0", + ) + add_version_to_release_channel.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + release_channel="test_channel", + version="3.0", + ) + set_release_directive.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + version="3.0", + patch=2, + release_channel="test_channel", + release_directive="test_directive", + target_accounts=None, + ) + + mock_console.warning.assert_called_once_with( + "Maximum number of versions in release channel reached. Removing version 2.0 from release_channel test_channel to make space for version 3.0." + ) + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +@mock.patch(SQL_FACADE_SHOW_RELEASE_DIRECTIVES) +@mock.patch(SQL_FACADE_SHOW_VERSIONS) +@mock.patch(SQL_FACADE_REMOVE_VERSION_FROM_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_ADD_VERSION_TO_RELEASE_CHANNEL) +@mock.patch(SQL_FACADE_SET_RELEASE_DIRECTIVE) +def test_given_only_one_version_referenced_by_existing_release_directive_when_publish_non_interactive_no_force_then_error( + set_release_directive, + add_version_to_release_channel, + remove_version_from_release_channel, + show_versions, + show_release_directives, + show_release_channels, + application_package_entity, + action_context, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [ + {"name": "TEST_CHANNEL", "versions": ["1.0", "2.0"]} + ] + show_release_directives.return_value = [ + {"name": "TEST_DIRECTIVE", "version": "1.0", "patch": 1} + ] + show_versions.return_value = [ + {"version": "1.0", "patch": 1, "created_on": datetime(2024, 12, 3)}, + {"version": "2.0", "patch": 1, "created_on": datetime(2024, 12, 5)}, + {"version": "3.0", "patch": 2, "created_on": datetime(2024, 12, 6)}, + ] + + with pytest.raises(UsageError) as e: + application_package_entity.action_publish( + action_ctx=action_context, + release_channel="test_channel", + release_directive="test_directive", + version="3.0", + patch=2, + interactive=False, + force=False, + ) + + assert ( + str(e.value) + == "Cannot proceed with publishing the new version. Please remove an existing version from the release channel to make space for the new version, or use --force to automatically clean up unused versions." + ) + + show_versions.assert_called_once_with(pkg_model.fqn.name, pkg_model.meta.role) + show_release_directives.assert_called_once_with( + package_name=pkg_model.fqn.name, + role=pkg_model.meta.role, + release_channel="test_channel", + ) + show_release_channels.assert_called_once_with( + pkg_model.fqn.name, pkg_model.meta.role + ) + remove_version_from_release_channel.assert_not_called() + add_version_to_release_channel.assert_not_called() + set_release_directive.assert_not_called() diff --git a/tests/nativeapp/test_sf_sql_facade.py b/tests/nativeapp/test_sf_sql_facade.py index a3fe853876..23e6a56ac7 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -228,7 +228,6 @@ def sut(): sut() -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) def test_execute_with_role_wh_db(mock_execute_queries, mock_execute_query, mock_cursor): # Arrange @@ -294,7 +293,6 @@ def test_execute_with_role_wh_db(mock_execute_queries, mock_execute_query, mock_ mock_parent.assert_has_calls(all_execute_calls) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) def test_execute_no_db(mock_execute_queries, mock_execute_query, mock_cursor): # Arrange @@ -346,7 +344,6 @@ def test_execute_no_db(mock_execute_queries, mock_execute_query, mock_cursor): mock_parent.assert_has_calls(all_execute_calls) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) def test_execute_no_wh(mock_execute_queries, mock_execute_query, mock_cursor): # Arrange @@ -398,7 +395,6 @@ def test_execute_no_wh(mock_execute_queries, mock_execute_query, mock_cursor): mock_parent.assert_has_calls(all_execute_calls) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) def test_execute_no_role(mock_execute_queries, mock_execute_query, mock_cursor): # Arrange @@ -453,7 +449,6 @@ def test_execute_no_role(mock_execute_queries, mock_execute_query, mock_cursor): mock_parent.assert_has_calls(all_execute_calls) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) def test_execute_no_wh_no_db(mock_execute_queries, mock_execute_query, mock_cursor): # Arrange @@ -495,7 +490,6 @@ def test_execute_no_wh_no_db(mock_execute_queries, mock_execute_query, mock_curs mock_parent.assert_has_calls(all_execute_calls) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) def test_execute_no_role_no_wh(mock_execute_queries, mock_execute_query, mock_cursor): # Arrange @@ -537,7 +531,6 @@ def test_execute_no_role_no_wh(mock_execute_queries, mock_execute_query, mock_cu mock_parent.assert_has_calls(all_execute_calls) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) def test_execute_no_role_no_db(mock_execute_queries, mock_execute_query, mock_cursor): # Arrange @@ -579,7 +572,6 @@ def test_execute_no_role_no_db(mock_execute_queries, mock_execute_query, mock_cu mock_parent.assert_has_calls(all_execute_calls) -@mock.patch(SQL_EXECUTOR_EXECUTE) @mock.patch(SQL_EXECUTOR_EXECUTE_QUERIES) def test_execute_no_role_no_wh_no_db(mock_execute_queries, mock_execute_query): # Arrange @@ -682,7 +674,6 @@ def test_execute_catch_all_exception( (UseObjectType.WAREHOUSE, "test_wh"), ], ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_object(mock_execute_query, object_type, object_name, mock_cursor): side_effects, expected = mock_execute_helper( [(None, mock.call(f"use {object_type} {object_name}"))] @@ -692,7 +683,6 @@ def test_use_object(mock_execute_query, object_type, object_name, mock_cursor): assert mock_execute_query.mock_calls == expected -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_object_catches_not_exists_error(mock_execute_query): object_type = UseObjectType.ROLE object_name = "test_err_role" @@ -713,7 +703,6 @@ def test_use_object_catches_not_exists_error(mock_execute_query): ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_object_catches_other_programming_error_raises_unknown_sql_error( mock_execute_query, ): @@ -736,7 +725,6 @@ def test_use_object_catches_other_programming_error_raises_unknown_sql_error( ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_object_catches_other_sql_error(mock_execute_query): object_type = UseObjectType.ROLE object_name = "test_err_role" @@ -749,7 +737,6 @@ def test_use_object_catches_other_sql_error(mock_execute_query): assert "Failed to use role test_err_role." in str(err) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_warehouse_single_quoted_id(mock_execute_query, mock_cursor): single_quoted_name = "test warehouse" side_effects, expected = mock_execute_helper( @@ -770,7 +757,6 @@ def test_use_warehouse_single_quoted_id(mock_execute_query, mock_cursor): assert mock_execute_query.mock_calls == expected -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_warehouse_same_id_single_quotes(mock_execute_query, mock_cursor): single_quoted_name = "test warehouse" side_effects, expected = mock_execute_helper( @@ -789,7 +775,6 @@ def test_use_warehouse_same_id_single_quotes(mock_execute_query, mock_cursor): assert mock_execute_query.mock_calls == expected -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_role_single_quoted_id(mock_execute_query, mock_cursor): single_quoted_name = "test role" side_effects, expected = mock_execute_helper( @@ -810,7 +795,6 @@ def test_use_role_single_quoted_id(mock_execute_query, mock_cursor): assert mock_execute_query.mock_calls == expected -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_role_same_id_single_quotes(mock_execute_query, mock_cursor): single_quoted_name = "test role" side_effects, expected = mock_execute_helper( @@ -829,7 +813,6 @@ def test_use_role_same_id_single_quotes(mock_execute_query, mock_cursor): assert mock_execute_query.mock_calls == expected -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_db_single_quoted_id(mock_execute_query, mock_cursor): single_quoted_name = "test db" side_effects, expected = mock_execute_helper( @@ -850,7 +833,6 @@ def test_use_db_single_quoted_id(mock_execute_query, mock_cursor): assert mock_execute_query.mock_calls == expected -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_db_same_id_single_quotes(mock_execute_query, mock_cursor): single_quoted_name = "test db" side_effects, expected = mock_execute_helper( @@ -904,7 +886,6 @@ def test_use_db_same_id_single_quotes(mock_execute_query, mock_cursor): ), ], ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_warehouse_bubbles_errors( mock_execute_query, error_raised, error_caught, error_message, mock_cursor ): @@ -962,7 +943,6 @@ def test_use_warehouse_bubbles_errors( ), ], ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_role_bubbles_errors( mock_execute_query, error_raised, error_caught, error_message, mock_cursor ): @@ -1020,7 +1000,6 @@ def test_use_role_bubbles_errors( ), ], ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_use_db_bubbles_errors( mock_execute_query, error_raised, error_caught, error_message, mock_cursor ): @@ -1043,7 +1022,6 @@ def test_use_db_bubbles_errors( assert error_message in str(err) -@mock.patch(SQL_EXECUTOR_EXECUTE) @pytest.mark.parametrize( "parameter_value,event_table", [ @@ -1076,7 +1054,6 @@ def test_account_event_table( assert sql_facade.get_account_event_table() == event_table -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_get_event_definitions_base_case(mock_execute_query, mock_cursor): app_name = "test_app" query = "show telemetry event definitions in application test_app" @@ -1104,7 +1081,6 @@ def test_get_event_definitions_base_case(mock_execute_query, mock_cursor): assert result == events_definitions -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_get_event_definitions_with_non_safe_identifier( mock_execute_query, mock_cursor ): @@ -1134,7 +1110,6 @@ def test_get_event_definitions_with_non_safe_identifier( assert result == events_definitions -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_get_event_definitions_with_role(mock_execute_query, mock_cursor): app_name = "test_app" role_name = "my_role" @@ -1169,7 +1144,6 @@ def test_get_event_definitions_with_role(mock_execute_query, mock_cursor): assert result == events_definitions -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_get_event_definitions_bubbles_errors(mock_execute_query): app_name = "test_app" query = "show telemetry event definitions in application test_app" @@ -1193,7 +1167,6 @@ def test_get_event_definitions_bubbles_errors(mock_execute_query): ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_get_app_properties_base_case(mock_execute_query, mock_cursor): app_name = "test_app" query = f"desc application {app_name}" @@ -1217,7 +1190,6 @@ def test_get_app_properties_base_case(mock_execute_query, mock_cursor): assert result == {"some_param": "param_value", "comment": "this is a test app"} -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_get_app_properties_with_non_safe_identifier(mock_execute_query, mock_cursor): app_name = "test.app" query = f'desc application "test.app"' @@ -1241,7 +1213,6 @@ def test_get_app_properties_with_non_safe_identifier(mock_execute_query, mock_cu assert result == {"some_param": "param_value", "comment": "this is a test app"} -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_get_app_properties_with_role(mock_execute_query, mock_cursor): app_name = "test_app" role_name = "my_role" @@ -1272,7 +1243,6 @@ def test_get_app_properties_with_role(mock_execute_query, mock_cursor): assert result == {"some_param": "param_value", "comment": "this is a test app"} -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_get_app_properties_bubbles_errors(mock_execute_query): app_name = "test_app" query = f"desc application {app_name}" @@ -1293,7 +1263,6 @@ def test_get_app_properties_bubbles_errors(mock_execute_query): assert f"Failed to describe application {app_name}. {error_message}" in str(err) -@mock.patch(SQL_EXECUTOR_EXECUTE) @pytest.mark.parametrize( "events, expected_result", [ @@ -1313,8 +1282,7 @@ def test_share_telemetry_events(mock_execute_query, events, expected_result): mock_execute_query.assert_called_once_with(expected_result) -@mock.patch(SQL_EXECUTOR_EXECUTE) -def test_share_telemtry_events_with_non_safe_identifier(mock_execute_query): +def test_share_telemetry_events_with_non_safe_identifier(mock_execute_query): app_name = "test.app" events = ["SNOWFLAKE$EVENT1", "SNOWFLAKE$EVENT2"] mock_execute_query.return_value = None @@ -2863,30 +2831,71 @@ def test_get_ui_parameter_with_no_value_then_use_default(mock_cursor): execute_str_mock.assert_called_once_with(expected_ui_params_query) -def test_show_release_directives_no_release_channel_specified(mock_execute_query): +def test_show_release_directives_no_release_channel_specified( + mock_execute_query, mock_cursor +): package_name = "test_package" expected_query = f"show release directives in application package {package_name}" + mock_cursor_results = [ + { + "name": "test_directive", + "created_on": datetime(2021, 2, 1), + "modified_on": datetime(2021, 4, 3), + "version": "v1", + "patch": 1, + } + ] + mock_execute_query.side_effect = [mock_cursor(mock_cursor_results, [])] - sql_facade.show_release_directives(package_name) + result = sql_facade.show_release_directives(package_name) + assert result == mock_cursor_results mock_execute_query.assert_called_once_with(expected_query, cursor_class=DictCursor) -def test_show_release_directive_with_release_channel_specified(mock_execute_query): +def test_show_release_directive_with_release_channel_specified( + mock_execute_query, mock_cursor +): package_name = "test_package" release_channel = "test_channel" expected_query = f"show release directives in application package {package_name} for release channel {release_channel}" - sql_facade.show_release_directives(package_name, release_channel) + mock_cursor_results = [ + { + "name": "test_directive", + "created_on": datetime(2021, 2, 1), + "modified_on": datetime(2021, 4, 3), + "version": "v1", + "patch": 1, + } + ] + mock_execute_query.side_effect = [mock_cursor(mock_cursor_results, [])] + result = sql_facade.show_release_directives(package_name, release_channel) + + assert result == mock_cursor_results mock_execute_query.assert_called_once_with(expected_query, cursor_class=DictCursor) -def test_show_release_directive_with_special_characters_in_names(mock_execute_query): +def test_show_release_directive_with_special_characters_in_names( + mock_execute_query, mock_cursor +): package_name = "test.package" release_channel = "test.channel" expected_query = f'show release directives in application package "{package_name}" for release channel "{release_channel}"' - sql_facade.show_release_directives(package_name, release_channel) + mock_cursor_results = [ + { + "name": "test_directive", + "created_on": datetime(2021, 2, 1), + "modified_on": datetime(2021, 4, 3), + "version": "v1", + "patch": 1, + } + ] + mock_execute_query.side_effect = [mock_cursor(mock_cursor_results, [])] + result = sql_facade.show_release_directives(package_name, release_channel) + + assert result == mock_cursor_results mock_execute_query.assert_called_once_with(expected_query, cursor_class=DictCursor) @@ -3335,7 +3344,6 @@ def test_set_default_release_directive_no_release_channel( ), ], ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_set_release_directive_errors( mock_execute_query, error_raised, error_caught, error_message ): @@ -3526,7 +3534,6 @@ def test_modify_default_release_directive_no_release_channel( ), ], ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_modify_release_directive_errors( mock_execute_query, error_raised, error_caught, error_message ): @@ -3918,7 +3925,6 @@ def test_add_accounts_to_release_channel_with_special_chars_in_names( ), ], ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_add_accounts_to_release_channel_error( mock_execute_query, error_raised, error_caught, error_message, mock_use_role ): @@ -4014,7 +4020,6 @@ def test_remove_accounts_from_release_channel_with_special_chars_in_names( ), ], ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_remove_accounts_from_release_channel_error( mock_execute_query, error_raised, error_caught, error_message, mock_use_role ): @@ -4100,7 +4105,6 @@ def test_add_version_to_release_channel_with_special_chars_in_names( ), ], ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_add_version_to_release_channel_error( mock_execute_query, error_raised, error_caught, error_message, mock_use_role ): @@ -4114,7 +4118,6 @@ def test_add_version_to_release_channel_error( assert error_message in str(err) -# same tests but for remove_version_from_release_channel def test_remove_version_from_release_channel_valid_input_then_success( mock_use_role, mock_execute_query ): @@ -4187,7 +4190,6 @@ def test_remove_version_from_release_channel_with_special_chars_in_names( ), ], ) -@mock.patch(SQL_EXECUTOR_EXECUTE) def test_remove_version_from_release_channel_error( mock_execute_query, error_raised, error_caught, error_message, mock_use_role ): @@ -4199,3 +4201,98 @@ def test_remove_version_from_release_channel_error( ) assert error_message in str(err) + + +def test_get_versions_valid_input_then_success( + mock_execute_query, mock_use_role, mock_cursor +): + package_name = "test_package" + role = "test_role" + expected_query = f"show versions in application package {package_name}" + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_value = [ + {"version": "v1", "patch": 0, "created_on": datetime(2021, 2, 1)}, + {"version": "v2", "patch": 1, "created_on": datetime(2021, 2, 2)}, + ] + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor(expected_value, []), + mock.call(expected_query, cursor_class=DictCursor), + ) + ] + ) + mock_execute_query.side_effect = side_effects + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + result = sql_facade.show_versions(package_name, role) + + assert result == expected_value + + +def test_get_versions_with_special_chars_in_names( + mock_execute_query, mock_use_role, mock_cursor +): + package_name = "test.package" + role = "test_role" + expected_query = f'show versions in application package "{package_name}"' + + expected_use_objects = [ + (mock_use_role, mock.call(role)), + ] + expected_value = [ + {"version": "v1", "patch": 0, "created_on": datetime(2021, 2, 1)}, + {"version": "v2", "patch": 1, "created_on": datetime(2021, 2, 2)}, + ] + + side_effects, expected = mock_execute_helper( + [ + ( + mock_cursor(expected_value, []), + mock.call(expected_query, cursor_class=DictCursor), + ) + ] + ) + mock_execute_query.side_effect = side_effects + expected_execute_query = [(mock_execute_query, call) for call in expected] + + with assert_in_context(expected_use_objects, expected_execute_query): + result = sql_facade.show_versions(package_name, role) + + assert result == expected_value + + +@pytest.mark.parametrize( + "error_raised, error_caught, error_message", + [ + ( + ProgrammingError(), + InvalidSQLError, + "Failed to show versions for application package test_package.", + ), + ( + DatabaseError("some database error"), + UnknownSQLError, + "Unknown SQL error occurred. Failed to show versions for application package test_package. some database error", + ), + ( + ProgrammingError(errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED), + UserInputError, + "Application package test_package does not exist or you are not authorized to access it.", + ), + ], +) +def test_get_versions_error( + mock_execute_query, error_raised, error_caught, error_message, mock_use_role +): + mock_execute_query.side_effect = error_raised + + with pytest.raises(error_caught) as err: + sql_facade.show_versions("test_package", "test_role") + + assert error_message in str(err) diff --git a/tests/nativeapp/utils.py b/tests/nativeapp/utils.py index 8db8953a4d..353731ec0e 100644 --- a/tests/nativeapp/utils.py +++ b/tests/nativeapp/utils.py @@ -96,6 +96,7 @@ SQL_FACADE_SHOW_RELEASE_CHANNELS = f"{SQL_FACADE}.show_release_channels" SQL_FACADE_DROP_VERSION = f"{SQL_FACADE}.drop_version_from_package" SQL_FACADE_CREATE_VERSION = f"{SQL_FACADE}.create_version_in_package" +SQL_FACADE_SHOW_VERSIONS = f"{SQL_FACADE}.show_versions" SQL_FACADE_ADD_ACCOUNTS_TO_RELEASE_CHANNEL = ( f"{SQL_FACADE}.add_accounts_to_release_channel" )