# 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 "