diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index f158788b7c..a3fc8c01db 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -26,6 +26,7 @@ * Add support for release channels feature in native app version creation/drop. * `snow app version create` now returns version, patch, and label in JSON format. * Add ability to specify release channel when creating application instance from release directive: `snow app run --from-release-directive --channel=` +* Add ability to list release channels through `snow app release-channel list` command ## 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 beb62bfee7..d33c0d2018 100644 --- a/src/snowflake/cli/_plugins/nativeapp/commands.py +++ b/src/snowflake/cli/_plugins/nativeapp/commands.py @@ -31,6 +31,9 @@ from snowflake.cli._plugins.nativeapp.entities.application_package import ( ApplicationPackageEntityModel, ) +from snowflake.cli._plugins.nativeapp.release_channel.commands import ( + app as release_channels_app, +) from snowflake.cli._plugins.nativeapp.release_directive.commands import ( app as release_directives_app, ) @@ -71,6 +74,7 @@ ) app.add_typer(versions_app) app.add_typer(release_directives_app) +app.add_typer(release_channels_app) log = logging.getLogger(__name__) diff --git a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py index 8f76c7ac4c..af95ce2e10 100644 --- a/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py +++ b/src/snowflake/cli/_plugins/nativeapp/entities/application_package.py @@ -53,6 +53,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.utils import needs_confirmation, sanitize_dir_name from snowflake.cli._plugins.snowpark.snowpark_entity_model import ( FunctionEntityModel, @@ -766,6 +767,69 @@ def action_release_directive_unset( role=self.role, ) + def _print_channel_to_console(self, channel: ReleaseChannel) -> None: + """ + Prints the release channel details to the console. + """ + console = self._workspace_ctx.console + + console.message(f"""[bold]{channel["name"]}[/bold]""") + accounts_list: Optional[list[str]] = channel["targets"].get("accounts") + target_accounts = ( + f"({', '.join(accounts_list)})" + if accounts_list is not None + else "ALL ACCOUNTS" + ) + + formatted_created_on = ( + channel["created_on"].astimezone().strftime("%Y-%m-%d %H:%M:%S.%f %Z") + if channel["created_on"] + else "" + ) + + formatted_updated_on = ( + channel["updated_on"].astimezone().strftime("%Y-%m-%d %H:%M:%S.%f %Z") + if channel["updated_on"] + else "" + ) + with console.indented(): + console.message(f"Description: {channel['description']}") + console.message(f"Versions: ({', '.join(channel['versions'])})") + console.message(f"Created on: {formatted_created_on}") + console.message(f"Updated on: {formatted_updated_on}") + console.message(f"Target accounts: {target_accounts}") + + def action_release_channel_list( + self, + action_ctx: ActionContext, + release_channel: Optional[str], + *args, + **kwargs, + ) -> list[ReleaseChannel]: + """ + Get all existing release channels for an application package. + If `release_channel` is provided, only the specified release channel is listed. + """ + console = self._workspace_ctx.console + available_channels = get_snowflake_facade().show_release_channels( + self.name, self.role + ) + + filtered_channels = [ + channel + for channel in available_channels + if release_channel is None + or same_identifiers(channel["name"], release_channel) + ] + + if not filtered_channels: + console.message("No release channels found.") + else: + for channel in filtered_channels: + self._print_channel_to_console(channel) + + return filtered_channels + def _bundle(self, action_ctx: ActionContext = None): model = self._entity_model bundle_map = build_bundle(self.project_root, self.deploy_root, model.artifacts) diff --git a/src/snowflake/cli/_plugins/nativeapp/release_channel/__init__.py b/src/snowflake/cli/_plugins/nativeapp/release_channel/__init__.py new file mode 100644 index 0000000000..ada0a4e13d --- /dev/null +++ b/src/snowflake/cli/_plugins/nativeapp/release_channel/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024 Snowflake Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. diff --git a/src/snowflake/cli/_plugins/nativeapp/release_channel/commands.py b/src/snowflake/cli/_plugins/nativeapp/release_channel/commands.py new file mode 100644 index 0000000000..4614f6e06b --- /dev/null +++ b/src/snowflake/cli/_plugins/nativeapp/release_channel/commands.py @@ -0,0 +1,71 @@ +# Copyright (c) 2024 Snowflake Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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 + +import logging +from typing import Optional + +import typer +from snowflake.cli._plugins.nativeapp.v2_conversions.compat import ( + force_project_definition_v2, +) +from snowflake.cli._plugins.workspace.manager import WorkspaceManager +from snowflake.cli.api.cli_global_context import get_cli_context +from snowflake.cli.api.commands.decorators import with_project_definition +from snowflake.cli.api.commands.snow_typer import SnowTyperFactory +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, +) + +app = SnowTyperFactory( + name="release-channel", + help="Manages release channels of an application package", +) + +log = logging.getLogger(__name__) + + +@app.command("list", requires_connection=True) +@with_project_definition() +@force_project_definition_v2() +def release_channel_list( + channel: Optional[str] = typer.Argument( + default=None, + show_default=False, + help="The release channel to list. If not provided, all release channels are listed.", + ), + **options, +) -> CommandResult: + """ + Lists the release channels available for an application package. + """ + + 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"] + channels = ws.perform_action( + package_id, + EntityActions.RELEASE_CHANNEL_LIST, + release_channel=channel, + ) + + if cli_context.output_format == OutputFormat.JSON: + return CollectionResult(channels) diff --git a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py index 16a8d43102..4c30ead793 100644 --- a/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +++ b/src/snowflake/cli/_plugins/nativeapp/sf_sql_facade.py @@ -13,11 +13,13 @@ # limitations under the License. from __future__ import annotations +import json import logging from contextlib import contextmanager +from datetime import datetime from functools import cache from textwrap import dedent -from typing import Any, Dict, List +from typing import Any, Dict, List, TypedDict from snowflake.cli._plugins.connection.util import UIParameter, get_ui_parameter from snowflake.cli._plugins.nativeapp.constants import ( @@ -52,6 +54,7 @@ CANNOT_DISABLE_MANDATORY_TELEMETRY, CANNOT_DISABLE_RELEASE_CHANNELS, DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, + DOES_NOT_EXIST_OR_NOT_AUTHORIZED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, RELEASE_DIRECTIVE_DOES_NOT_EXIST, @@ -73,6 +76,18 @@ from snowflake.cli.api.utils.cursor import find_first_row from snowflake.connector import DictCursor, ProgrammingError +ReleaseChannel = TypedDict( + "ReleaseChannel", + { + "name": str, + "description": str, + "created_on": datetime, + "updated_on": datetime, + "targets": dict[str, Any], + "versions": list[str], + }, +) + class SnowflakeSQLFacade: def __init__(self, sql_executor: BaseSqlExecutor | None = None): @@ -1141,7 +1156,7 @@ def unset_release_directive( def show_release_channels( self, package_name: str, role: str | None = None - ) -> list[dict[str, Any]]: + ) -> list[ReleaseChannel]: """ Show release channels in a package. @param package_name: Name of the package @@ -1155,6 +1170,7 @@ def show_release_channels( return [] package_identifier = to_identifier(package_name) + results = [] with self._use_role_optional(role): try: cursor = self._sql_executor.execute_query( @@ -1166,11 +1182,31 @@ def show_release_channels( if err.errno == SQL_COMPILATION_ERROR: # Release not out yet and param not out yet return [] + 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 release channels for application package {package_name}.", ) - return cursor.fetchall() + rows = cursor.fetchall() + + for row in rows: + targets = json.loads(row["targets"]) if row.get("targets") else {} + versions = json.loads(row["versions"]) if row.get("versions") else [] + results.append( + ReleaseChannel( + name=row["name"], + description=row["description"], + created_on=row["created_on"], + updated_on=row["updated_on"], + targets=targets, + versions=versions, + ) + ) + + return results def _strip_empty_lines(text: str) -> str: diff --git a/tests/__snapshots__/test_help_messages.ambr b/tests/__snapshots__/test_help_messages.ambr index e367fb918f..91f09a4acf 100644 --- a/tests/__snapshots__/test_help_messages.ambr +++ b/tests/__snapshots__/test_help_messages.ambr @@ -578,6 +578,123 @@ +------------------------------------------------------------------------------+ + ''' +# --- +# name: test_help_messages[app.release-channel.list] + ''' + + Usage: default app release-channel list [OPTIONS] [CHANNEL] + + Lists the release channels available for an application package. + + +- Arguments ------------------------------------------------------------------+ + | channel [CHANNEL] The release channel to list. If not provided, all | + | release channels are listed. | + +------------------------------------------------------------------------------+ + +- Options --------------------------------------------------------------------+ + | --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] + ''' + + Usage: default app release-channel [OPTIONS] COMMAND [ARGS]... + + Manages release channels of an application package + + +- Options --------------------------------------------------------------------+ + | --help -h Show this message and exit. | + +------------------------------------------------------------------------------+ + +- Commands -------------------------------------------------------------------+ + | list Lists the release channels available for an application package. | + +------------------------------------------------------------------------------+ + + ''' # --- # name: test_help_messages[app.release-directive.list] @@ -1777,6 +1894,7 @@ | configured in Snowflake. | | open Opens the Snowflake Native App inside of your browser, | | once it has been installed in your account. | + | 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 | | account, uploads code files to its stage, then creates | @@ -10122,6 +10240,23 @@ +------------------------------------------------------------------------------+ + ''' +# --- +# name: test_help_messages_no_help_flag[app.release-channel] + ''' + + Usage: default app release-channel [OPTIONS] COMMAND [ARGS]... + + Manages release channels of an application package + + +- Options --------------------------------------------------------------------+ + | --help -h Show this message and exit. | + +------------------------------------------------------------------------------+ + +- Commands -------------------------------------------------------------------+ + | list Lists the release channels available for an application package. | + +------------------------------------------------------------------------------+ + + ''' # --- # name: test_help_messages_no_help_flag[app.release-directive] @@ -10190,6 +10325,7 @@ | configured in Snowflake. | | open Opens the Snowflake Native App inside of your browser, | | once it has been installed in your account. | + | 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 | | account, uploads code files to its stage, then creates | diff --git a/tests/nativeapp/__snapshots__/test_application_package_entity.ambr b/tests/nativeapp/__snapshots__/test_application_package_entity.ambr new file mode 100644 index 0000000000..30b56e7b2d --- /dev/null +++ b/tests/nativeapp/__snapshots__/test_application_package_entity.ambr @@ -0,0 +1,46 @@ +# serializer version: 1 +# name: test_given_no_release_channels_when_list_release_channels_then_success + ''' + No release channels found. + + ''' +# --- +# name: test_given_release_channel_with_no_target_account_or_version_then_show_all_accounts_in_snapshot + ''' + channel1 + Description: desc + Versions: () + Created on: 2024-12-03 00:00:00.000000 UTC + Updated on: 2024-12-05 00:00:00.000000 UTC + Target accounts: ALL ACCOUNTS + + ''' +# --- +# name: test_given_release_channels_with_a_selected_channel_to_filter_when_list_release_channels_then_returned_selected_channel + ''' + channel1 + Description: desc + Versions: (v1, v2) + Created on: 2024-12-03 00:00:00.000000 UTC + Updated on: 2024-12-05 00:00:00.000000 UTC + Target accounts: (org1.acc1, org2.acc2) + + ''' +# --- +# name: test_given_release_channels_with_proper_values_when_list_release_channels_then_success + ''' + channel1 + Description: desc + Versions: (v1, v2) + Created on: 2024-12-03 00:00:00.000000 UTC + Updated on: 2024-12-05 00:00:00.000000 UTC + Target accounts: (org1.acc1, org2.acc2) + channel2 + Description: desc2 + Versions: (v3) + Created on: 2024-12-03 00:00:00.000000 UTC + Updated on: 2024-12-05 00:00:00.000000 UTC + Target accounts: (org3.acc3) + + ''' +# --- diff --git a/tests/nativeapp/test_application_package_entity.py b/tests/nativeapp/test_application_package_entity.py index 0772a5ada0..bbab822962 100644 --- a/tests/nativeapp/test_application_package_entity.py +++ b/tests/nativeapp/test_application_package_entity.py @@ -13,10 +13,12 @@ # limitations under the License. from __future__ import annotations +from datetime import datetime from pathlib import Path from unittest import mock import pytest +import pytz import yaml from click import ClickException from snowflake.cli._plugins.connection.util import UIParameter @@ -29,6 +31,7 @@ ApplicationPackageEntityModel, ) from snowflake.cli._plugins.workspace.context import ActionContext, WorkspaceContext +from snowflake.cli.api.console import cli_console from snowflake.connector.cursor import DictCursor from tests.nativeapp.utils import ( @@ -824,7 +827,7 @@ def test_given_channels_enabled_and_non_existing_channel_selected_when_release_d @mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS, return_value=[{"name": "default"}]) @mock.patch(SQL_FACADE_UNSET_RELEASE_DIRECTIVE) -def test_given_default_directive_selected_when_directive_unset_then_error( +def test_given_default_directive_selected_when_release_directive_unset_then_error( unset_release_directive, show_release_channels, application_package_entity, @@ -848,3 +851,174 @@ def test_given_default_directive_selected_when_directive_unset_then_error( show_release_channels.assert_not_called() unset_release_directive.assert_not_called() + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +def test_given_release_channels_with_proper_values_when_list_release_channels_then_success( + show_release_channels, + application_package_entity, + action_context, + capsys, + os_agnostic_snapshot, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + application_package_entity._workspace_ctx.console = cli_console # noqa SLF001 + + pkg_model.meta.role = "package_role" + + created_on_mock = mock.MagicMock() + updated_on_mock = mock.MagicMock() + created_on_mock.astimezone.return_value = datetime( + year=2024, month=12, day=3, tzinfo=pytz.utc + ) + updated_on_mock.astimezone.return_value = datetime( + year=2024, month=12, day=5, tzinfo=pytz.utc + ) + + release_channels = [ + { + "name": "channel1", + "description": "desc", + "created_on": created_on_mock, + "updated_on": updated_on_mock, + "versions": ["v1", "v2"], + "targets": {"accounts": ["org1.acc1", "org2.acc2"]}, + }, + { + "name": "channel2", + "description": "desc2", + "created_on": created_on_mock, + "updated_on": updated_on_mock, + "versions": ["v3"], + "targets": {"accounts": ["org3.acc3"]}, + }, + ] + show_release_channels.return_value = release_channels + + result = application_package_entity.action_release_channel_list( + action_context, release_channel=None + ) + captured = capsys.readouterr() + + assert result == release_channels + assert captured.out == os_agnostic_snapshot + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +def test_given_release_channel_with_no_target_account_or_version_then_show_all_accounts_in_snapshot( + show_release_channels, + application_package_entity, + action_context, + capsys, + os_agnostic_snapshot, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + application_package_entity._workspace_ctx.console = cli_console # noqa SLF001 + + pkg_model.meta.role = "package_role" + + created_on_mock = mock.MagicMock() + updated_on_mock = mock.MagicMock() + created_on_mock.astimezone.return_value = datetime( + year=2024, month=12, day=3, tzinfo=pytz.utc + ) + updated_on_mock.astimezone.return_value = datetime( + year=2024, month=12, day=5, tzinfo=pytz.utc + ) + + release_channels = [ + { + "name": "channel1", + "description": "desc", + "created_on": created_on_mock, + "updated_on": updated_on_mock, + "versions": [], + "targets": {}, + } + ] + + show_release_channels.return_value = release_channels + + result = application_package_entity.action_release_channel_list( + action_context, release_channel=None + ) + captured = capsys.readouterr() + + assert result == release_channels + assert captured.out == os_agnostic_snapshot + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +def test_given_no_release_channels_when_list_release_channels_then_success( + show_release_channels, + application_package_entity, + action_context, + capsys, + os_agnostic_snapshot, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + application_package_entity._workspace_ctx.console = cli_console # noqa SLF001 + + pkg_model.meta.role = "package_role" + + show_release_channels.return_value = [] + + result = application_package_entity.action_release_channel_list( + action_context, release_channel=None + ) + captured = capsys.readouterr() + + assert result == [] + assert captured.out == os_agnostic_snapshot + + +@mock.patch(SQL_FACADE_SHOW_RELEASE_CHANNELS) +def test_given_release_channels_with_a_selected_channel_to_filter_when_list_release_channels_then_returned_selected_channel( + show_release_channels, + application_package_entity, + action_context, + capsys, + os_agnostic_snapshot, +): + pkg_model = application_package_entity._entity_model # noqa SLF001 + application_package_entity._workspace_ctx.console = cli_console # noqa SLF001 + + pkg_model.meta.role = "package_role" + + created_on_mock = mock.MagicMock() + updated_on_mock = mock.MagicMock() + created_on_mock.astimezone.return_value = datetime( + year=2024, month=12, day=3, tzinfo=pytz.utc + ) + updated_on_mock.astimezone.return_value = datetime( + year=2024, month=12, day=5, tzinfo=pytz.utc + ) + + test_channel_1 = { + "name": "channel1", + "description": "desc", + "created_on": created_on_mock, + "updated_on": updated_on_mock, + "versions": ["v1", "v2"], + "targets": {"accounts": ["org1.acc1", "org2.acc2"]}, + } + + test_channel_2 = { + "name": "channel2", + "description": "desc2", + "created_on": created_on_mock, + "updated_on": updated_on_mock, + "versions": ["v3"], + "targets": {"accounts": ["org3.acc3"]}, + } + show_release_channels.return_value = [ + test_channel_1, + test_channel_2, + ] + + result = application_package_entity.action_release_channel_list( + action_context, release_channel="channel1" + ) + + assert result == [test_channel_1] + assert capsys.readouterr().out == os_agnostic_snapshot diff --git a/tests/nativeapp/test_sf_sql_facade.py b/tests/nativeapp/test_sf_sql_facade.py index bcdf4cd0e8..7d678c669d 100644 --- a/tests/nativeapp/test_sf_sql_facade.py +++ b/tests/nativeapp/test_sf_sql_facade.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from contextlib import contextmanager +from datetime import datetime from textwrap import dedent from unittest import mock from unittest.mock import _Call as Call @@ -51,6 +52,7 @@ CANNOT_DISABLE_MANDATORY_TELEMETRY, CANNOT_DISABLE_RELEASE_CHANNELS, DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED, + DOES_NOT_EXIST_OR_NOT_AUTHORIZED, INSUFFICIENT_PRIVILEGES, NO_WAREHOUSE_SELECTED_IN_SESSION, RELEASE_DIRECTIVE_DOES_NOT_EXIST, @@ -2917,16 +2919,68 @@ def test_show_release_channels_when_feature_enabled( ) mock_cursor_results = [ { - "NAME": "test_channel", - "VERSIONS": '["V1"]', - "TARGETS": '{"accounts": []}', + "name": "test_channel", + "description": "test_description", + "versions": '["V1"]', + "created_on": datetime(2021, 2, 1), + "updated_on": datetime(2021, 4, 3), + "targets": '{"accounts": ["org1.acc1", "org2.acc2"]}', } ] mock_execute_query.side_effect = [mock_cursor(mock_cursor_results, [])] result = sql_facade.show_release_channels(package_name) - assert result == mock_cursor_results + # assert result is same as mock_cursor_results except for keys "targets" and "versions": + assert result == [ + { + "name": "test_channel", + "description": "test_description", + "created_on": datetime(2021, 2, 1), + "updated_on": datetime(2021, 4, 3), + "targets": {"accounts": ["org1.acc1", "org2.acc2"]}, + "versions": ["V1"], + } + ] + + mock_get_ui_parameter.assert_called_once_with( + UIParameter.NA_FEATURE_RELEASE_CHANNELS, True + ) + mock_execute_query.assert_called_once_with(expected_query, cursor_class=DictCursor) + + +@mock.patch(SQL_FACADE_GET_UI_PARAMETER, return_value=True) +def test_show_release_channels_with_no_accounts_and_no_versions( + mock_get_ui_parameter, mock_execute_query, mock_cursor +): + package_name = "test_package" + + expected_query = f"show release channels in application package {package_name}" + mock_cursor_results = [ + { + "name": "test_channel", + "description": "test_description", + "created_on": datetime(2021, 2, 1), + "updated_on": datetime(2021, 4, 3), + "targets": "", + "versions": "[]", + } + ] + mock_execute_query.side_effect = [mock_cursor(mock_cursor_results, [])] + + result = sql_facade.show_release_channels(package_name) + + assert result == [ + { + "name": "test_channel", + "description": "test_description", + "created_on": datetime(2021, 2, 1), + "updated_on": datetime(2021, 4, 3), + "targets": {}, + "versions": [], + } + ] + mock_get_ui_parameter.assert_called_once_with( UIParameter.NA_FEATURE_RELEASE_CHANNELS, True ) @@ -2951,6 +3005,26 @@ def test_show_release_channels_when_error( mock_execute_query.assert_called_once_with(expected_query, cursor_class=DictCursor) +@mock.patch(SQL_FACADE_GET_UI_PARAMETER, return_value=True) +def test_show_release_channels_when_package_does_not_exist( + mock_get_ui_parameter, mock_execute_query, mock_cursor +): + package_name = "test_package" + + expected_query = f"show release channels in application package {package_name}" + mock_execute_query.side_effect = ProgrammingError( + errno=DOES_NOT_EXIST_OR_NOT_AUTHORIZED + ) + + with pytest.raises(UserInputError): + sql_facade.show_release_channels(package_name) + + mock_get_ui_parameter.assert_called_once_with( + UIParameter.NA_FEATURE_RELEASE_CHANNELS, True + ) + mock_execute_query.assert_called_once_with(expected_query, cursor_class=DictCursor) + + def test_unset_release_directive_with_release_channel( mock_execute_query, ):