diff --git a/docs/guides/share-observability-report/send-report-summary.mdx b/docs/guides/share-observability-report/send-report-summary.mdx index d136bf9c5..da5fb59d2 100644 --- a/docs/guides/share-observability-report/send-report-summary.mdx +++ b/docs/guides/share-observability-report/send-report-summary.mdx @@ -11,51 +11,38 @@ This is useful for you and your team members to understand quickly if there is s - **Hosting on S3 or GCS**: You need to provide slack token and channel to in order to enable the Slack results summary. - Demo + Demo +### Enabling report summary for hosted report -### Enabling report summary for hosted report - After you [set up a Slack app and token](/integrations/slack#slack-integration-setup) you can run the following command: AWS S3: + ```shell edr send-report --aws-profile-name --s3-bucket-name --slack-token --slack-channel-name ``` GCS: -```shell -edr send-report --google-service-account-path --gcs-bucket-name --slack-token --slack-channel-name -``` -A link to the hosted report is included in the summary, which can be accessed based on the permissions you have set on your hosting platform (AWS/GCS). +```shell +edr send-report --google-service-account-path --gcs-bucket-name --slack-token --slack-channel-name +``` +A link to the hosted report is included in the summary, which can be accessed based on the permissions you have set on your hosting platform (AWS/GCS). -### Report summary configuration +### Report summary configuration #### Filter results summary You can filter your results summary by a specific tag, owner or model. To filter by tag: + ```shell send-report selectors edr send-report --select tag:finance edr send-report --select config.meta.owner:@jeff edr send-report --select model:customers edr send-report --select customers ``` - -#### Include test descriptions - -For a quick high level overview and to avoid reaching Slack's row limit, the default test results summary does not include test descriptions. -The test descriptions can be included in your summary, but it is only recommended if you have a small number of failures. - -The following `include` configuration will allow you to add descriptions: - -```shell -edr send-report --include descriptions -``` diff --git a/docs/pics/report_slack_summary.png b/docs/pics/report_slack_summary.png index 8f07c93ee..0fb3e378e 100644 Binary files a/docs/pics/report_slack_summary.png and b/docs/pics/report_slack_summary.png differ diff --git a/docs/quickstart/send-slack-alerts.mdx b/docs/quickstart/send-slack-alerts.mdx index 2d37bdf39..436d5b935 100644 --- a/docs/quickstart/send-slack-alerts.mdx +++ b/docs/quickstart/send-slack-alerts.mdx @@ -354,7 +354,7 @@ Elementary support configuring suppression interval for alerts. By default, the suppression interval for all of the alerts is set to 0. Elementary won't send any alert that is generated within suppression interval. -`alert_suppression_interval` can accept values of 0-24 (including unrounded numbers) - this number represents the number of hours for which alerts will be skipped. +`alert_suppression_interval` can accept values greater than 0, including unrounded numbers - this number represents the number of hours for which alerts will be skipped. To set it up globaly for your project, add the alert suppression interval to your models and tests in the `dbt_project.yml` file: diff --git a/elementary/monitor/api/alerts/alerts.py b/elementary/monitor/api/alerts/alerts.py index 23fe32dab..174222237 100644 --- a/elementary/monitor/api/alerts/alerts.py +++ b/elementary/monitor/api/alerts/alerts.py @@ -170,7 +170,7 @@ def _get_suppressed_alerts( else None ) is_alert_in_suppression = ( - (current_time_utc - last_sent_time).seconds / 3600 + (current_time_utc - last_sent_time).total_seconds() / 3600 <= suppression_interval if last_sent_time else False diff --git a/elementary/monitor/api/test_management/__init__.py b/elementary/monitor/api/test_management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/elementary/monitor/api/test_management/schema.py b/elementary/monitor/api/test_management/schema.py new file mode 100644 index 000000000..e69de29bb diff --git a/elementary/monitor/api/test_management/test_management.py b/elementary/monitor/api/test_management/test_management.py new file mode 100644 index 000000000..ece295332 --- /dev/null +++ b/elementary/monitor/api/test_management/test_management.py @@ -0,0 +1,44 @@ +from elementary.clients.api.api_client import APIClient +from elementary.clients.dbt.dbt_runner import DbtRunner +from elementary.monitor.fetchers.test_management.schema import ( + ResourcesModel, + TagsModel, + TestsModel, + UserModel, + UsersModel, +) +from elementary.monitor.fetchers.test_management.test_management import ( + TestManagementFetcher, +) +from elementary.utils.log import get_logger + +logger = get_logger(__name__) + + +class TestManagementAPI(APIClient): + def __init__( + self, + dbt_runner: DbtRunner, + exclude_elementary: bool = True, + ): + super().__init__(dbt_runner) + self.test_management_fetcher = TestManagementFetcher(dbt_runner=self.dbt_runner) + self.exclude_elementary = exclude_elementary + + def get_resources(self) -> ResourcesModel: + return self.test_management_fetcher.get_resources(self.exclude_elementary) + + def get_tests(self) -> TestsModel: + tests = self.test_management_fetcher.get_tests() + return TestsModel(tests=tests) + + def get_tags(self) -> TagsModel: + return self.test_management_fetcher.get_tags() + + def get_project_users(self) -> UsersModel: + project_users = self.test_management_fetcher.get_all_project_users() + project_users = [ + UserModel(name=project_user, origin="project") + for project_user in project_users + ] + return UsersModel(users=project_users) diff --git a/elementary/monitor/cli.py b/elementary/monitor/cli.py index 53ba97f99..2efb62e4a 100644 --- a/elementary/monitor/cli.py +++ b/elementary/monitor/cli.py @@ -217,7 +217,7 @@ def get_cli_properties() -> dict: @click.option( "--group-by", type=click.Choice(["alert", "table"]), - default="alert", + default=None, help="Whether to group alerts by 'alert' or by 'table'", ) @click.pass_context diff --git a/elementary/monitor/data_monitoring/report/slack_report_summary_message_builder.py b/elementary/monitor/data_monitoring/report/slack_report_summary_message_builder.py index 86afcbe93..d2511dc53 100644 --- a/elementary/monitor/data_monitoring/report/slack_report_summary_message_builder.py +++ b/elementary/monitor/data_monitoring/report/slack_report_summary_message_builder.py @@ -21,14 +21,13 @@ def get_slack_message( filter: SelectorFilterSchema = SelectorFilterSchema(), include_description: bool = False, ) -> SlackMessageSchema: - self._add_title_to_slack_alert( - test_results=test_results, - bucket_website_url=bucket_website_url, + self._add_title_to_slack_alert(env=env) + self._add_preview_to_slack_alert( + test_results, days_back=days_back, - env=env, + bucket_website_url=bucket_website_url, filter=filter, ) - self._add_preview_to_slack_alert(test_results) self._add_details_to_slack_alert( test_results=test_results, include_description=include_description, @@ -38,44 +37,18 @@ def get_slack_message( def _add_title_to_slack_alert( self, - test_results: List[TestResultSummarySchema], env: str, - days_back: int, - bucket_website_url: Optional[str] = None, - filter: SelectorFilterSchema = SelectorFilterSchema(), ): env_text = ( ":construction: Development" if env == "dev" else ":large_green_circle: Production" ) - summary_filter_text = self._get_summary_filter_text(days_back, filter) - totals = self._get_test_results_totals(test_results) title_blocks = [ self.create_header_block(f":mag: Monitoring summary ({env_text})"), - self.create_text_section_block(summary_filter_text), + self.create_divider_block(), ] - - if bucket_website_url: - title_blocks.append( - self.create_text_section_block( - f"<{bucket_website_url}|View full report> :arrow_upper_right:" - ) - ) - - title_blocks.append( - self.create_fields_section_block( - [ - f":white_check_mark: Passed: {totals.get('passed', 0)}", - f":small_red_triangle: Failed: {totals.get('failed', 0)}", - f":exclamation: Errors: {totals.get('error', 0)}", - f":Warning: Warning: {totals.get('warning', 0)}", - ] - ) - ) - - title_blocks.append(self.create_divider_block()) self._add_always_displayed_blocks(title_blocks) @staticmethod @@ -96,37 +69,42 @@ def _get_summary_filter_text( return f"_This summary was generated with the following filters - {days_back_text}{f', {selector_text}' if selector_text else ''}_" - def _add_preview_to_slack_alert(self, test_results: List[TestResultSummarySchema]): - owners = [] - tags = [] - subscribers = [] - for test in test_results: - if test.status != "pass": - formatted_tags = [ - tag if tag.startswith(TAG_PREFIX) else f"{TAG_PREFIX}{tag}" - for tag in test.tags + def _add_preview_to_slack_alert( + self, + test_results: List[TestResultSummarySchema], + days_back: int, + filter: SelectorFilterSchema = SelectorFilterSchema(), + bucket_website_url: Optional[str] = None, + ): + preview_blocks = [] + + summary_filter_text = self._get_summary_filter_text(days_back, filter) + preview_blocks.append(self.create_text_section_block(summary_filter_text)) + + if bucket_website_url: + preview_blocks.append( + self.create_text_section_block( + f"<{bucket_website_url}|View full report> :arrow_upper_right:" + ) + ) + + totals = self._get_test_results_totals(test_results) + preview_blocks.append( + self.create_fields_section_block( + [ + f":white_check_mark: Passed: {totals.get('passed', 0)}", + f":small_red_triangle: Failed: {totals.get('failed', 0)}", + f":exclamation: Errors: {totals.get('error', 0)}", + f":Warning: Warning: {totals.get('warning', 0)}", ] - owners.extend(test.owners) - tags.extend(formatted_tags) - subscribers.extend(test.subscribers) - - tags_text = self.prettify_and_dedup_list(tags) if tags else "_No tags_" - owners_text = self.prettify_and_dedup_list(owners) if owners else "_No owners_" - subscribers_text = ( - self.prettify_and_dedup_list(subscribers) - if subscribers - else "_No subscribers_" + ) ) - preview_blocks = [ - self.create_text_section_block(":mega: *Attention required* :mega:"), - self.create_text_section_block(f"*Tags:* {tags_text}"), - self.create_text_section_block(f"*Owners:* {owners_text}"), - self.create_text_section_block(f"*Subscribers:* {subscribers_text}"), - self.create_empty_section_block(), - ] - if preview_blocks: - self._add_blocks_as_attachments(preview_blocks) + preview_blocks_filler = [self.create_empty_section_block()] * ( + self._MAX_ALERT_PREVIEW_BLOCKS - len(preview_blocks) + ) + preview_blocks.extend(preview_blocks_filler) + self._add_blocks_as_attachments(preview_blocks) def _add_details_to_slack_alert( self, diff --git a/elementary/monitor/dbt_project/macros/base_queries/owners.sql b/elementary/monitor/dbt_project/macros/base_queries/owners.sql new file mode 100644 index 000000000..5c7f36418 --- /dev/null +++ b/elementary/monitor/dbt_project/macros/base_queries/owners.sql @@ -0,0 +1,29 @@ +{% macro project_owners() %} + {% set project_owners_query %} + with dbt_models as ( + select * from {{ ref('elementary', 'dbt_models') }} + ), + + dbt_sources as ( + select * from {{ ref('elementary', 'dbt_sources') }} + ), + + dbt_seeds as ( + select * from {{ ref('elementary', 'dbt_seeds') }} + ), + + dbt_tests as ( + select * from {{ ref('elementary', 'dbt_tests') }} + ) + + select model_owners as owner from dbt_tests + union + select owner from dbt_models + union + select owner from dbt_sources + union + select owner from dbt_seeds + {% endset %} + {% set owners_agate = run_query(project_owners_query) %} + {% do return(elementary.agate_to_dicts(owners_agate)) %} +{% endmacro %} diff --git a/elementary/monitor/dbt_project/macros/base_queries/resources.sql b/elementary/monitor/dbt_project/macros/base_queries/resources.sql new file mode 100644 index 000000000..fbf120a4e --- /dev/null +++ b/elementary/monitor/dbt_project/macros/base_queries/resources.sql @@ -0,0 +1,86 @@ +{% macro model_resources(exclude_elementary=true) %} + {% set model_resources_query %} + with dbt_models as ( + select * from {{ ref('elementary', 'dbt_models') }} + ) + + select + name, + schema_name as schema, + tags, + owner as owners + from dbt_models + {% if exclude_elementary %} + where package_name != 'elementary' + {% endif %} + {% endset %} + {% set models_agate = run_query(model_resources_query) %} + {% do return(elementary.agate_to_dicts(models_agate)) %} +{% endmacro %} + + +{% macro source_resources(exclude_elementary=true) %} + {% set source_resources_query %} + with dbt_sources as ( + select * from {{ ref('elementary', 'dbt_sources') }} + ) + + select + name, + source_name, + schema_name AS schema, + tags, + owner AS owners + from dbt_sources + {% if exclude_elementary %} + where package_name != 'elementary' + {% endif %} + {% endset %} + {% set sources_agate = run_query(source_resources_query) %} + {% do return(elementary.agate_to_dicts(sources_agate)) %} +{% endmacro %} + + +{% macro all_resources(exclude_elementary=true) %} + {% set models = model_resources(exclude_elementary) %} + {% set sources = source_resources(exclude_elementary) %} + + {% set resources = [] %} + {% do resources.extend(sources) %} + {% for model in models %} + {% do model.update({"source_name": none}) %} + {% do resources.append(model) %} + {% endfor %} + {% do return(resources) %} +{% endmacro %} + + +{% macro resources_meta() %} + {% set resources_meta_query %} + with dbt_models as ( + select * from {{ ref('elementary', 'dbt_models') }} + ), + + dbt_sources as ( + select * from {{ ref('elementary', 'dbt_sources') }} + ), + + dbt_seeds as ( + select * from {{ ref('elementary', 'dbt_seeds') }} + ), + + dbt_tests as ( + select * from {{ ref('elementary', 'dbt_tests') }} + ) + + select meta from dbt_tests + union + select meta from dbt_models + union + select meta from dbt_sources + union + select meta from dbt_seeds + {% endset %} + {% set resources_meta_agate = run_query(resources_meta_query) %} + {% do return(elementary.agate_to_dicts(resources_meta_agate)) %} +{% endmacro %} diff --git a/elementary/monitor/dbt_project/macros/base_queries/tags.sql b/elementary/monitor/dbt_project/macros/base_queries/tags.sql new file mode 100644 index 000000000..c1d1209b9 --- /dev/null +++ b/elementary/monitor/dbt_project/macros/base_queries/tags.sql @@ -0,0 +1,23 @@ +{% macro project_tags() %} + {% set project_tags_query %} + with dbt_models as ( + select * from {{ ref('elementary', 'dbt_models') }} + ), + + dbt_sources as ( + select * from {{ ref('elementary', 'dbt_sources') }} + ), + + dbt_tests as ( + select * from {{ ref('elementary', 'dbt_tests') }} + ) + + select tags from dbt_models + union + select tags from dbt_sources + union + select tags from dbt_tests + {% endset %} + {% set tags_agate = run_query(project_tags_query) %} + {% do return(elementary.agate_to_dicts(tags_agate)) %} +{% endmacro %} diff --git a/elementary/monitor/dbt_project/macros/base_queries/tests.sql b/elementary/monitor/dbt_project/macros/base_queries/tests.sql new file mode 100644 index 000000000..120441118 --- /dev/null +++ b/elementary/monitor/dbt_project/macros/base_queries/tests.sql @@ -0,0 +1,28 @@ +{% macro get_tests() %} + {% set tests_query %} + with dbt_tests as ( + select * from {{ ref('elementary', 'dbt_tests') }} + ), + + dbt_models as ( + select * from {{ ref('elementary', 'dbt_models') }} + ) + + select + dbt_tests.unique_id as id, + dbt_tests.schema_name as schema, + dbt_models.name as table, + dbt_tests.test_column_name as column, + dbt_tests.test_namespace as test_package, + dbt_tests.short_name as test_name, + dbt_tests.test_params as test_params, + dbt_tests.severity as severity, + dbt_tests.model_owners as model_owners, + dbt_tests.model_tags as model_tags, + dbt_tests.tags as tags, + dbt_tests.generated_at as generated_at + from dbt_tests left join dbt_models on dbt_tests.parent_model_unique_id = dbt_models.unique_id + {% endset %} + {% set tests_agate = run_query(tests_query) %} + {% do return(elementary.agate_to_dicts(tests_agate)) %} +{% endmacro %} diff --git a/elementary/monitor/fetchers/models/schema.py b/elementary/monitor/fetchers/models/schema.py index c705f6991..ce60f7748 100644 --- a/elementary/monitor/fetchers/models/schema.py +++ b/elementary/monitor/fetchers/models/schema.py @@ -1,6 +1,6 @@ import os import posixpath -from typing import List, Optional +from typing import Any, Dict, List, Optional from pydantic import Field, validator @@ -32,6 +32,7 @@ class ArtifactSchema(ExtendedBaseModel): package_name: Optional[str] = None description: Optional[str] = None full_path: Optional[str] = None + meta: Optional[Dict[str, Any]] = None @validator("tags", pre=True) def load_tags(cls, tags): diff --git a/elementary/monitor/fetchers/test_management/__init__.py b/elementary/monitor/fetchers/test_management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/elementary/monitor/fetchers/test_management/schema.py b/elementary/monitor/fetchers/test_management/schema.py new file mode 100644 index 000000000..9a8baffca --- /dev/null +++ b/elementary/monitor/fetchers/test_management/schema.py @@ -0,0 +1,63 @@ +from typing import List, Literal, Optional + +from pydantic import BaseModel, Field, ValidationError, validator + + +class ResourceColumnModel(BaseModel): + name: str + type: str + + +class ResourceModel(BaseModel): + name: str + source_name: Optional[str] + db_schema: str = Field(alias="schema") + tags: List[str] = Field(default_factory=list) + owners: List[str] = Field(default_factory=list) + columns: List[ResourceColumnModel] = Field(default_factory=list) + + +class ResourcesModel(BaseModel): + models: List[ResourceModel] = Field(default_factory=list) + sources: List[ResourceModel] = Field(default_factory=list) + + +class TestModel(BaseModel): + id: str + db_schema: str = Field(alias="schema") + table: Optional[str] + column: Optional[str] + package: Optional[str] = None + name: str + type: Optional[str] + args: Optional[dict] + severity: str + owners: List[str] = Field(default_factory=list) + tags: List[str] = Field(default_factory=list) + updated_at: str + updated_by: Optional[str] + + @validator("severity", pre=True) + def validate_severity(cls, severity: str): + severity_lower_string = severity.lower() + if severity_lower_string not in ["error", "warn"]: + raise ValidationError('Severity must be "warn" or "error"') + return severity_lower_string + + +class TestsModel(BaseModel): + tests: List[TestModel] = Field(default_factory=list) + + +class TagsModel(BaseModel): + tags: List[str] = Field(default_factory=list) + + +class UserModel(BaseModel): + name: str + email: Optional[str] + origin: Literal["account", "project"] + + +class UsersModel(BaseModel): + users: List[UserModel] = Field(default_factory=list) diff --git a/elementary/monitor/fetchers/test_management/test_management.py b/elementary/monitor/fetchers/test_management/test_management.py new file mode 100644 index 000000000..dae8debde --- /dev/null +++ b/elementary/monitor/fetchers/test_management/test_management.py @@ -0,0 +1,163 @@ +import json +from typing import List + +from elementary.clients.dbt.dbt_runner import DbtRunner +from elementary.clients.fetcher.fetcher import FetcherClient +from elementary.monitor.fetchers.test_management.schema import ( + ResourceModel, + ResourcesModel, + TagsModel, + TestModel, +) +from elementary.utils.json_utils import unpack_and_flatten_str_to_list +from elementary.utils.log import get_logger + +logger = get_logger(__name__) + + +class TestManagementFetcher(FetcherClient): + def __init__(self, dbt_runner: DbtRunner): + super().__init__(dbt_runner) + + def get_models(self, exclude_elementary=True) -> List[ResourceModel]: + run_operation_response = self.dbt_runner.run_operation( + macro_name="model_resources", + macro_args=dict(exclude_elementary=exclude_elementary), + ) + models_results = ( + json.loads(run_operation_response[0]) if run_operation_response else [] + ) + models = [] + for model_result in models_results: + owners = unpack_and_flatten_str_to_list(model_result["owners"]) + models.append( + ResourceModel( + name=model_result["name"], + schema=model_result["schema"], + tags=json.loads(model_result["tags"]), + owners=owners, + ) + ) + return models + + def get_sources(self, exclude_elementary=True) -> List[ResourceModel]: + run_operation_response = self.dbt_runner.run_operation( + macro_name="source_resources", + macro_args=dict(exclude_elementary=exclude_elementary), + ) + sources_results = ( + json.loads(run_operation_response[0]) if run_operation_response else [] + ) + sources = [] + for source_result in sources_results: + owners = unpack_and_flatten_str_to_list(source_result["owners"]) + sources.append( + ResourceModel( + name=source_result["name"], + source_name=source_result["source_name"], + schema=source_result["schema"], + tags=json.loads(source_result["tags"]), + owners=owners, + ) + ) + return sources + + def get_resources(self, exclude_elementary=True) -> ResourcesModel: + models = self.get_models(exclude_elementary) + sources = self.get_sources(exclude_elementary) + return ResourcesModel(models=models, sources=sources) + + def get_tags(self) -> TagsModel: + run_operation_response = self.dbt_runner.run_operation( + macro_name="project_tags" + ) + tags_results = ( + json.loads(run_operation_response[0]) if run_operation_response else [] + ) + all_tags = [] + for tags_result in tags_results: + tags = json.loads(tags_result["tags"]) + all_tags.extend(tags) + return TagsModel(tags=all_tags) + + def get_tests(self) -> List[TestModel]: + run_operation_response = self.dbt_runner.run_operation(macro_name="get_tests") + test_results = ( + json.loads(run_operation_response[0]) if run_operation_response else [] + ) + tests = [] + for test_result in test_results: + owners = unpack_and_flatten_str_to_list(test_result["model_owners"]) + tags = list( + set( + [ + *json.loads(test_result["model_tags"]), + *json.loads(test_result["tags"]), + ] + ) + ) + + tests.append( + TestModel( + id=test_result["id"], + schema=test_result["schema"], + table=test_result["table"], + column=test_result["column"], + package=test_result["test_package"], + name=test_result["test_name"], + args=json.loads(test_result["test_params"]), + severity=test_result["severity"], + owners=owners, + tags=tags, + updated_at=test_result["generated_at"], + ) + ) + return tests + + def get_project_owners(self) -> List[str]: + run_operation_response = self.dbt_runner.run_operation( + macro_name="project_owners" + ) + owners_results = ( + json.loads(run_operation_response[0]) if run_operation_response else [] + ) + all_owners = [] + for owners_result in owners_results: + owners = owners_result["owner"] + if owners is None: + continue + owners = unpack_and_flatten_str_to_list(owners) + all_owners.extend(owners) + return all_owners + + def get_project_subscribers(self) -> List[str]: + run_operation_response = self.dbt_runner.run_operation( + macro_name="resources_meta" + ) + resources_meta_results = ( + json.loads(run_operation_response[0]) if run_operation_response else [] + ) + all_subscribers = [] + for resources_meta_result in resources_meta_results: + stringfy_meta = resources_meta_result["meta"] + if stringfy_meta: + meta = json.loads(stringfy_meta) + subscribers = meta.get( + "subscribers", + meta.get("alerts_config", {}).get("subscribers", []), + ) + if type(subscribers) is str: + try: + subscribers = json.loads(subscribers) + except json.JSONDecodeError: + subscribers = subscribers.split(",") + for subscriber in subscribers: + all_subscribers.append(subscriber.strip()) + return all_subscribers + + def get_all_project_users(self) -> List[str]: + project_users = [ + *self.get_project_owners(), + *self.get_project_subscribers(), + ] + return list(set(project_users)) diff --git a/tests/unit/monitor/data_monitoring/report/test_slack_report_summary_message_builder.py b/tests/unit/monitor/data_monitoring/report/test_slack_report_summary_message_builder.py index ecbe29cb1..82f4fa198 100644 --- a/tests/unit/monitor/data_monitoring/report/test_slack_report_summary_message_builder.py +++ b/tests/unit/monitor/data_monitoring/report/test_slack_report_summary_message_builder.py @@ -52,35 +52,12 @@ def test_add_details_to_slack_alert_attachments_limit(test_results_summary): assert "The amount of results exceeded Slack" in attachments_as_string -def test_owners_tags_and_subscribers_of_passed_tests_are_filtered_out( - test_results_summary, -): - # Within attachments limitation - message_builder = SlackReportSummaryMessageBuilder() - message_builder._add_preview_to_slack_alert(test_results_summary) - attachments_as_string = json.dumps( - message_builder.slack_message.get("attachments")[0].get("blocks") - ) - assert "Jeff" in attachments_as_string - assert "Joe" in attachments_as_string - assert "Ron" in attachments_as_string - assert "subscriber1" in attachments_as_string - assert "subscriber2" in attachments_as_string - assert "production" in attachments_as_string - assert "dev" in attachments_as_string - - assert "Jack" not in attachments_as_string - assert "subscriber22" not in attachments_as_string - assert "staging" not in attachments_as_string - - def test_passed_tests_filtered_out_of_details_view( test_results_summary, ): # Within attachments limitation message_builder = SlackReportSummaryMessageBuilder() passed_tests_from_fixture = [x for x in test_results_summary if x.status == "pass"] - message_builder._add_preview_to_slack_alert(passed_tests_from_fixture) message_builder._add_details_to_slack_alert(passed_tests_from_fixture) attachments_as_string = json.dumps( message_builder.slack_message.get("attachments")[0].get("blocks")