# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you 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 from datetime import datetime from unittest.mock import MagicMock, patch from superset.dashboards.excel_export import email REQUESTED = datetime(2026, 1, 1, 12, 0, 0) EXPIRES = datetime(2026, 1, 2, 12, 0, 0) def test_success_email_contains_link_and_expiry() -> None: html = email.build_success_email( dashboard_title="Sales", download_url="https://signed.example/file.xlsx?sig=abc", requested_at=REQUESTED, expires_at=EXPIRES, ttl_seconds=86400, errored={}, ) assert "https://signed.example/file.xlsx?sig=abc" in html assert "expires in 24 hours" in html assert "2026-01-02 12:00:00 UTC" in html assert "2026-01-01 12:00:00 UTC" in html assert "
  • " not in html # no skipped section def test_success_email_sub_hour_ttl_reports_minutes() -> None: # A sub-hour TTL must not truncate to "0 hours"; it should report minutes. html = email.build_success_email( dashboard_title="Sales", download_url="https://x", requested_at=REQUESTED, expires_at=EXPIRES, ttl_seconds=900, errored={}, ) assert "expires in 15 minutes" in html assert "0 hours" not in html def test_success_email_mixed_ttl_reports_hours_and_minutes() -> None: html = email.build_success_email( dashboard_title="Sales", download_url="https://x", requested_at=REQUESTED, expires_at=EXPIRES, ttl_seconds=5400, errored={}, ) assert "expires in 1 hour 30 minutes" in html def test_success_email_lists_charts_with_no_query_context() -> None: html = email.build_success_email( dashboard_title="Sales", download_url="https://x", requested_at=REQUESTED, expires_at=EXPIRES, ttl_seconds=86400, errored={email.ERROR_NO_QUERY_CONTEXT: ["10 - Broken chart"]}, ) assert "no saved query context" in html assert "
  • 10 - Broken chart
  • " in html def test_success_email_groups_errors_by_reason() -> None: html = email.build_success_email( dashboard_title="Sales", download_url="https://x", requested_at=REQUESTED, expires_at=EXPIRES, ttl_seconds=86400, errored={ email.ERROR_NO_QUERY_CONTEXT: ["10 - NoContext"], email.ERROR_GENERAL: ["30 - Boom"], }, ) # Each reason renders its own labelled section with the right chart. assert "no saved query context" in html assert "an error occurred" in html assert "
  • 10 - NoContext
  • " in html assert "
  • 30 - Boom
  • " in html def test_success_email_omits_empty_reason_groups() -> None: html = email.build_success_email( dashboard_title="Sales", download_url="https://x", requested_at=REQUESTED, expires_at=EXPIRES, ttl_seconds=86400, errored={email.ERROR_GENERAL: ["30 - Boom"]}, ) assert "an error occurred" in html assert "no saved query context" not in html assert "
  • 30 - Boom
  • " in html def test_success_email_escapes_title() -> None: html = email.build_success_email( dashboard_title="", download_url="https://x", requested_at=REQUESTED, expires_at=EXPIRES, ttl_seconds=86400, errored={}, ) assert "