diff --git a/.github/workflows/confluence-daily-dte.yaml b/.github/workflows/confluence-daily-dte.yaml index 94487707..88a35f17 100644 --- a/.github/workflows/confluence-daily-dte.yaml +++ b/.github/workflows/confluence-daily-dte.yaml @@ -44,10 +44,6 @@ jobs: echo "TESTRAIL_USERNAME=${{ secrets.TESTRAIL_USERNAME }}" >> $GITHUB_ENV echo "TESTRAIL_PASSWORD=${{ secrets.TESTRAIL_PASSWORD }}" >> $GITHUB_ENV echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV - echo "JIRA_HOST=${{ secrets.JIRA_HOST }}" >> $GITHUB_ENV - echo "JIRA_USER=${{ secrets.JIRA_USER }}" >> $GITHUB_ENV - echo "JIRA_PASSWORD=${{ secrets.JIRA_PASSWORD }}" >> $GITHUB_ENV - echo "BUGZILLA_API_KEY=${{ secrets.BUGZILLA_API_KEY }}" >> $GITHUB_ENV echo "ATLASSIAN_API_TOKEN=${{ secrets.ATLASSIAN_API_TOKEN }}" >> $GITHUB_ENV diff --git a/.github/workflows/confluence-daily.yaml b/.github/workflows/confluence-daily.yaml index 5244bb5e..12b30430 100644 --- a/.github/workflows/confluence-daily.yaml +++ b/.github/workflows/confluence-daily.yaml @@ -38,9 +38,6 @@ jobs: echo "TESTRAIL_USERNAME=${{ secrets.TESTRAIL_USERNAME }}" >> $GITHUB_ENV echo "TESTRAIL_PASSWORD=${{ secrets.TESTRAIL_PASSWORD }}" >> $GITHUB_ENV echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV - echo "JIRA_HOST=${{ secrets.JIRA_HOST }}" >> $GITHUB_ENV - echo "JIRA_USER=${{ secrets.JIRA_USER }}" >> $GITHUB_ENV - echo "JIRA_PASSWORD=${{ secrets.JIRA_PASSWORD }}" >> $GITHUB_ENV echo "BUGZILLA_API_KEY=${{ secrets.BUGZILLA_API_KEY }}" >> $GITHUB_ENV diff --git a/.github/workflows/preflight-push.yaml b/.github/workflows/preflight-push.yaml index b6507d4d..5be0cdf2 100644 --- a/.github/workflows/preflight-push.yaml +++ b/.github/workflows/preflight-push.yaml @@ -26,9 +26,6 @@ env: ATLASSIAN_API_TOKEN: ${{ secrets.ATLASSIAN_API_TOKEN }} ATLASSIAN_HOST: ${{ secrets.ATLASSIAN_HOST }} ATLASSIAN_USERNAME: ${{ secrets.ATLASSIAN_USERNAME }} - JIRA_HOST: ${{ secrets.JIRA_HOST }} - JIRA_USER: ${{ secrets.JIRA_USER }} - JIRA_PASSWORD: ${{ secrets.JIRA_PASSWORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUGZILLA_API_KEY: ${{ secrets.BUGZILLA_API_KEY }} BITRISE_HOST: ${{ secrets.BITRISE_HOST }} diff --git a/.github/workflows/production-daily-desktop.yaml b/.github/workflows/production-daily-desktop.yaml index 2f1e9065..26db1c71 100644 --- a/.github/workflows/production-daily-desktop.yaml +++ b/.github/workflows/production-daily-desktop.yaml @@ -57,9 +57,6 @@ jobs: echo "TESTRAIL_USERNAME=${{ secrets.TESTRAIL_USERNAME }}" >> $GITHUB_ENV echo "TESTRAIL_PASSWORD=${{ secrets.TESTRAIL_PASSWORD }}" >> $GITHUB_ENV echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV - echo "JIRA_HOST=${{ secrets.JIRA_HOST }}" >> $GITHUB_ENV - echo "JIRA_USER=${{ secrets.JIRA_USER }}" >> $GITHUB_ENV - echo "JIRA_PASSWORD=${{ secrets.JIRA_PASSWORD }}" >> $GITHUB_ENV echo "BUGZILLA_API_KEY=${{ secrets.BUGZILLA_API_KEY }}" >> $GITHUB_ENV echo "ATLASSIAN_API_TOKEN=${{ secrets.ATLASSIAN_API_TOKEN }}" >> $GITHUB_ENV echo "ATLASSIAN_HOST=${{ secrets.ATLASSIAN_HOST }}" >> $GITHUB_ENV diff --git a/.github/workflows/production-daily.yml b/.github/workflows/production-daily.yml index f180e6c7..8879cc89 100644 --- a/.github/workflows/production-daily.yml +++ b/.github/workflows/production-daily.yml @@ -22,9 +22,6 @@ env: ATLASSIAN_API_TOKEN: ${{ secrets.ATLASSIAN_API_TOKEN }} ATLASSIAN_HOST: ${{ secrets.ATLASSIAN_HOST }} ATLASSIAN_USERNAME: ${{ secrets.ATLASSIAN_USERNAME }} - JIRA_HOST: ${{ secrets.JIRA_HOST }} - JIRA_USER: ${{ secrets.JIRA_USER }} - JIRA_PASSWORD: ${{ secrets.JIRA_PASSWORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUGZILLA_API_KEY: ${{ secrets.BUGZILLA_API_KEY }} BITRISE_HOST: ${{ secrets.BITRISE_HOST }} diff --git a/.github/workflows/production-weekly-desktop.yaml b/.github/workflows/production-weekly-desktop.yaml index 5564e65d..1eaeb122 100644 --- a/.github/workflows/production-weekly-desktop.yaml +++ b/.github/workflows/production-weekly-desktop.yaml @@ -57,9 +57,6 @@ jobs: echo "ATLASSIAN_HOST=${{ secrets.ATLASSIAN_HOST }}" >> $GITHUB_ENV echo "ATLASSIAN_USERNAME=${{ secrets.ATLASSIAN_USERNAME }}" >> $GITHUB_ENV echo "BUGZILLA_API_KEY=${{ secrets.BUGZILLA_API_KEY }}" >> $GITHUB_ENV - echo "JIRA_HOST=${{ secrets.JIRA_HOST }}" >> $GITHUB_ENV - echo "JIRA_USER=${{ secrets.JIRA_USER }}" >> $GITHUB_ENV - echo "JIRA_PASSWORD=${{ secrets.JIRA_PASSWORD }}" >> $GITHUB_ENV - run: python ./__main__.py ${{ matrix.args }} continue-on-error: true diff --git a/.github/workflows/production-weekly-firefox-ios-deepdive.yaml b/.github/workflows/production-weekly-firefox-ios-deepdive.yaml index 2322420e..a119b1e5 100644 --- a/.github/workflows/production-weekly-firefox-ios-deepdive.yaml +++ b/.github/workflows/production-weekly-firefox-ios-deepdive.yaml @@ -23,9 +23,6 @@ env: ATLASSIAN_API_TOKEN: ${{ secrets.ATLASSIAN_API_TOKEN }} ATLASSIAN_HOST: ${{ secrets.ATLASSIAN_HOST }} ATLASSIAN_USERNAME: ${{ secrets.ATLASSIAN_USERNAME }} - JIRA_HOST: ${{ secrets.JIRA_HOST }} - JIRA_USER: ${{ secrets.JIRA_USER }} - JIRA_PASSWORD: ${{ secrets.JIRA_PASSWORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUGZILLA_API_KEY: ${{ secrets.BUGZILLA_API_KEY }} BITRISE_HOST: ${{ secrets.BITRISE_HOST }} diff --git a/.github/workflows/production-weekly.yml b/.github/workflows/production-weekly.yml index 028adfc8..4cd29a2a 100644 --- a/.github/workflows/production-weekly.yml +++ b/.github/workflows/production-weekly.yml @@ -44,9 +44,6 @@ jobs: echo "ATLASSIAN_API_TOKEN=${{ secrets.ATLASSIAN_API_TOKEN }}" >> $GITHUB_ENV echo "ATLASSIAN_HOST=${{ secrets.ATLASSIAN_HOST }}" >> $GITHUB_ENV echo "ATLASSIAN_USERNAME=${{ secrets.ATLASSIAN_USERNAME }}" >> $GITHUB_ENV - echo "JIRA_HOST=${{ secrets.JIRA_HOST }}" >> $GITHUB_ENV - echo "JIRA_USER=${{ secrets.JIRA_USER }}" >> $GITHUB_ENV - echo "JIRA_PASSWORD=${{ secrets.JIRA_PASSWORD }}" >> $GITHUB_ENV #- name: Update DB - test runs # run: python ./__main__.py --report-type test-run-counts --project ALL --num-days 7 diff --git a/.github/workflows/staging-daily-desktop.yml b/.github/workflows/staging-daily-desktop.yml index 870e630c..83f8f12b 100644 --- a/.github/workflows/staging-daily-desktop.yml +++ b/.github/workflows/staging-daily-desktop.yml @@ -55,9 +55,6 @@ jobs: echo "TESTRAIL_USERNAME=${{ secrets.TESTRAIL_USERNAME }}" >> $GITHUB_ENV echo "TESTRAIL_PASSWORD=${{ secrets.TESTRAIL_PASSWORD }}" >> $GITHUB_ENV echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV - echo "JIRA_HOST=${{ secrets.JIRA_HOST }}" >> $GITHUB_ENV - echo "JIRA_USER=${{ secrets.JIRA_USER }}" >> $GITHUB_ENV - echo "JIRA_PASSWORD=${{ secrets.JIRA_PASSWORD }}" >> $GITHUB_ENV echo "BUGZILLA_API_KEY=${{ secrets.BUGZILLA_API_KEY }}" >> $GITHUB_ENV echo "ATLASSIAN_API_TOKEN=${{ secrets.ATLASSIAN_API_TOKEN }}" >> $GITHUB_ENV echo "ATLASSIAN_HOST=${{ secrets.ATLASSIAN_HOST }}" >> $GITHUB_ENV diff --git a/.github/workflows/staging-daily.yml b/.github/workflows/staging-daily.yml index 7eacac07..89192f27 100644 --- a/.github/workflows/staging-daily.yml +++ b/.github/workflows/staging-daily.yml @@ -23,9 +23,6 @@ env: ATLASSIAN_API_TOKEN: ${{ secrets.ATLASSIAN_API_TOKEN }} ATLASSIAN_HOST: ${{ secrets.ATLASSIAN_HOST }} ATLASSIAN_USERNAME: ${{ secrets.ATLASSIAN_USERNAME }} - JIRA_HOST: ${{ secrets.JIRA_HOST }} - JIRA_USER: ${{ secrets.JIRA_USER }} - JIRA_PASSWORD: ${{ secrets.JIRA_PASSWORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUGZILLA_API_KEY: ${{ secrets.BUGZILLA_API_KEY }} BITRISE_HOST: ${{ secrets.BITRISE_HOST }} diff --git a/.github/workflows/staging-weekly-desktop.yml b/.github/workflows/staging-weekly-desktop.yml index 48f12123..b4ec238b 100644 --- a/.github/workflows/staging-weekly-desktop.yml +++ b/.github/workflows/staging-weekly-desktop.yml @@ -57,9 +57,6 @@ jobs: echo "ATLASSIAN_HOST=${{ secrets.ATLASSIAN_HOST }}" >> $GITHUB_ENV echo "ATLASSIAN_USERNAME=${{ secrets.ATLASSIAN_USERNAME }}" >> $GITHUB_ENV echo "BUGZILLA_API_KEY=${{ secrets.BUGZILLA_API_KEY }}" >> $GITHUB_ENV - echo "JIRA_HOST=${{ secrets.JIRA_HOST }}" >> $GITHUB_ENV - echo "JIRA_USER=${{ secrets.JIRA_USER }}" >> $GITHUB_ENV - echo "JIRA_PASSWORD=${{ secrets.JIRA_PASSWORD }}" >> $GITHUB_ENV - run: python ./__main__.py ${{ matrix.args }} continue-on-error: true diff --git a/.github/workflows/staging-weekly.yml b/.github/workflows/staging-weekly.yml index 41bcf26d..0f8b533e 100644 --- a/.github/workflows/staging-weekly.yml +++ b/.github/workflows/staging-weekly.yml @@ -23,9 +23,6 @@ env: ATLASSIAN_API_TOKEN: ${{ secrets.ATLASSIAN_API_TOKEN }} ATLASSIAN_HOST: ${{ secrets.ATLASSIAN_HOST }} ATLASSIAN_USERNAME: ${{ secrets.ATLASSIAN_USERNAME }} - JIRA_HOST: ${{ secrets.JIRA_HOST }} - JIRA_USER: ${{ secrets.JIRA_USER }} - JIRA_PASSWORD: ${{ secrets.JIRA_PASSWORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUGZILLA_API_KEY: ${{ secrets.BUGZILLA_API_KEY }} BITRISE_HOST: ${{ secrets.BITRISE_HOST }} diff --git a/.github/workflows/unit-tests-daily.yml b/.github/workflows/unit-tests-daily.yml new file mode 100644 index 00000000..6b4bd7bf --- /dev/null +++ b/.github/workflows/unit-tests-daily.yml @@ -0,0 +1,47 @@ +name: Unit Tests - Daily + +# Daily @9am UTC + manual trigger +on: + schedule: + - cron: "0 9 * * *" + workflow_dispatch: + +env: + ATLASSIAN_USERNAME: ${{ secrets.ATLASSIAN_USERNAME }} + ATLASSIAN_API_TOKEN: ${{ secrets.ATLASSIAN_API_TOKEN }} + +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - name: Check out source repository + uses: actions/checkout@v6 + + - name: Set up Python environment + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run unit tests + run: python -m unittest discover -s tests -v + + - name: Set job log URL + if: always() + run: echo "JOB_LOG_URL=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_ENV + + - name: Send workflow status to Slack + if: always() + uses: slackapi/slack-github-action@v3.0.1 + env: + WORKFLOW_NAME: ${{ github.workflow }} + BRANCH: ${{ github.head_ref || github.ref_name }} + JOB_STATUS: ${{ job.status == 'success' && ':white_check_mark:' || ':x:' }} + JOB_STATUS_COLOR: ${{ job.status == 'success' && '#36a64f' || '#FF0000' }} + with: + webhook: ${{ secrets.SLACK_WEBHOOK_URL_MOBILE_ALERTS_TOOLING }} + webhook-type: webhook-trigger + payload-templated: true + payload-file-path: "./config/payload-slack-content.json" diff --git a/api/jira/client.py b/api/jira/client.py index 76ded5c2..09d63b9e 100644 --- a/api/jira/client.py +++ b/api/jira/client.py @@ -18,7 +18,6 @@ FILTER_ID_ALL_REQUEST_ISSUE_TYPE, FILTER_ID_QA_NEEDED_iOS, FIREFOX_RELEASE_TRAIN, - HOST_JIRA, MAX_RESULT, QATT_BOARD, QATT_PARENT_TICKETS_IN_BOARD, @@ -33,11 +32,10 @@ class Jira: def __init__(self): try: - # _url_host = os.environ['JIRA_HOST'] - _url_host = f"https://{HOST_JIRA}/rest/api/3" + _url_host = f"https://{os.environ['ATLASSIAN_HOST']}/rest/api/3" self.client = JiraAPIClient(_url_host) - self.client.user = os.environ['JIRA_USER'] - self.client.password = os.environ['JIRA_PASSWORD'] + self.client.user = os.environ['ATLASSIAN_USERNAME'] + self.client.password = os.environ['ATLASSIAN_API_TOKEN'] except KeyError: print("ERROR: Missing jira env var") sys.exit(1) diff --git a/api/jira/report_qa_requests.py b/api/jira/report_qa_requests.py index 02288bcc..79566e08 100644 --- a/api/jira/report_qa_requests.py +++ b/api/jira/report_qa_requests.py @@ -55,8 +55,10 @@ def jira_qa_requests(): df = prepare_jira_df(payload) if df.empty: - logger.warning("Jira filtersreturned empty payload; no DB delete/insert.") - return + raise ValueError( + "jira_qa_requests returned empty payload — " + "check Jira credentials or filter. Database was not modified." + ) jira_delete(ReportJiraQARequests) @@ -92,8 +94,10 @@ def jira_qa_requests_workload(): df = prepare_jira_df(payload) if df.empty: - logger.warning("Empty payload; skipping DB delete/insert.") - return + raise ValueError( + "jira_qa_requests_workload returned empty payload — " + "check Jira credentials or filter. Database was not modified." + ) jira_delete(ReportJIraQARequestsNewIssueType) diff --git a/api/jira/report_qa_requests_desktop.py b/api/jira/report_qa_requests_desktop.py index 5cc0c659..ce5ba0c1 100644 --- a/api/jira/report_qa_requests_desktop.py +++ b/api/jira/report_qa_requests_desktop.py @@ -79,8 +79,10 @@ def jira_qa_requests_desktop(): df = prepare_jira_df(payload) if df.empty: - logger.warning("Jira filtersreturned empty payload; no DB delete/insert.") - return + raise ValueError( + "jira_qa_requests_desktop returned empty payload — " + "check Jira credentials or filter. Database was not modified." + ) jira_delete(ReportJiraQARequestsDesktop) diff --git a/api/jira/report_worklogs.py b/api/jira/report_worklogs.py index 20388002..a39da60e 100644 --- a/api/jira/report_worklogs.py +++ b/api/jira/report_worklogs.py @@ -42,6 +42,12 @@ def jira_worklogs(): worklog_data = [] issues = jira.filter_sv_parent_in_board() + if not issues: + raise ValueError( + "No issues returned from QATT board — " + "check Jira credentials or filter. Database was not modified." + ) + for issue in issues: parent_key = (issue.get("fields", {}).get("parent") or {}).get("key", issue.get("key")) # noqa parent_name = issue.get("fields", {}).get("summary", "Unknown") @@ -139,6 +145,12 @@ def jira_worklogs(): # FIX: Replace NaN values with None for MySQL compatibility df = df.astype(object).where(df.notna(), None) + if df.empty: + raise ValueError( + "Issues were fetched but no worklog data found — " + "Database was not modified." + ) + jira_delete(ReportJiraSoftvisionWorklogs) report_jira_worklogs_insert(df) diff --git a/constants.py b/constants.py index c88b9fbd..9bd43771 100644 --- a/constants.py +++ b/constants.py @@ -65,9 +65,6 @@ "sentry-rates", ] -# Jira Host -HOST_JIRA = "mozilla-hub.atlassian.net" - # JQL query options SEARCH = "search/jql" ISSUES = "issues" diff --git a/tests/test_jira_api_client.py b/tests/test_jira_api_client.py index dc86fff2..dc704862 100644 --- a/tests/test_jira_api_client.py +++ b/tests/test_jira_api_client.py @@ -4,19 +4,26 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +import os +import sys import unittest import requests from unittest.mock import MagicMock, patch from lib.jira_conn import JiraAPIClient -from constants import HOST_JIRA -JIRA_HOST = f"https://{HOST_JIRA}/rest/api/3/" +# Prevent database.py from connecting to MySQL at import time during unit tests. +# database.py runs autoload_with=pool at module level (to reflect table schemas), +# which requires a live DB connection. Unit tests don't have one. +if 'database' not in sys.modules: + sys.modules['database'] = MagicMock() + +ATLASSIAN_BASE_URL = f"https://{os.environ['ATLASSIAN_HOST']}/rest/api/3/" class TestsJiraAPIClient(unittest.TestCase): def setUp(self): - self.client = JiraAPIClient(JIRA_HOST) + self.client = JiraAPIClient(ATLASSIAN_BASE_URL) self.client.user = "" self.client.password = "" @@ -41,7 +48,7 @@ def test_get_search_url_construction(self, mock_get): # Verify the full URL passed to requests.get called_url = mock_get.call_args.args[0] - expected_url = f"{JIRA_HOST}search/jql?jql=project=MTE" + expected_url = f"{ATLASSIAN_BASE_URL}search/jql?jql=project=MTE" self.assertEqual(called_url, expected_url) @patch("lib.jira_conn.requests.get") @@ -120,7 +127,7 @@ def test_get_search_worklog_url_construction(self, mock_get): # Verify the full URL passed to requests.get called_url = mock_get.call_args.args[0] - expected_url = f"{JIRA_HOST}issue/MTE-123/worklog" + expected_url = f"{ATLASSIAN_BASE_URL}issue/MTE-123/worklog" self.assertEqual(called_url, expected_url) @patch("lib.jira_conn.requests.get") @@ -194,7 +201,7 @@ def test_get_search_default_endpoint_url_construction(self, mock_get): # Verify the full URL passed to requests.get called_url = mock_get.call_args.args[0] - expected_url = f"{JIRA_HOST}project" + expected_url = f"{ATLASSIAN_BASE_URL}project" self.assertEqual(called_url, expected_url) @patch("lib.jira_conn.requests.get") @@ -264,3 +271,121 @@ def test_base_url_keeps_trailing_slash(self): self.assertTrue(client_with_slash._JiraAPIClient__url.endswith('/')) # Should not have double slash self.assertFalse(client_with_slash._JiraAPIClient__url.endswith('//')) + + +class TestJiraCredentialsIntegration(unittest.TestCase): + """ + Integration test — hits the real Jira API using ATLASSIAN_USERNAME and + ATLASSIAN_API_TOKEN env vars. Fails if credentials are missing, expired, + or revoked. + """ + def setUp(self): + self.user = os.environ["ATLASSIAN_USERNAME"] + self.password = os.environ["ATLASSIAN_API_TOKEN"] + self.base_url = ATLASSIAN_BASE_URL + + def test_credentials_are_valid(self): + r = requests.get( + self.base_url + "myself", + headers={"Accept": "application/json"}, + auth=(self.user, self.password), + timeout=60, + ) + self.assertEqual( + r.status_code, 200, + f"Auth failed for {self.user} — {r.status_code}: {r.text}" + ) + + +class TestJiraWorklogs(unittest.TestCase): + + @patch("api.jira.report_worklogs._jira") + def test_raises_if_no_issues_returned(self, mock_jira): + """If the board returns 0 issues, raise ValueError and do not touch the DB.""" + from api.jira.report_worklogs import jira_worklogs + + mock_jira.return_value.filter_sv_parent_in_board.return_value = [] + + with self.assertRaises(ValueError) as ctx: + jira_worklogs() + + self.assertIn("No issues returned", str(ctx.exception)) + + @patch("api.jira.report_worklogs.jira_delete") + @patch("api.jira.report_worklogs._jira") + def test_db_not_cleared_when_no_issues(self, mock_jira, mock_delete): + """jira_delete must not be called if 0 issues are returned.""" + from api.jira.report_worklogs import jira_worklogs + + mock_jira.return_value.filter_sv_parent_in_board.return_value = [] + + try: + jira_worklogs() + except ValueError: + pass + + mock_delete.assert_not_called() + + @patch("api.jira.report_worklogs.jira_delete") + @patch("api.jira.report_worklogs._jira") + def test_db_not_cleared_when_no_worklogs(self, mock_jira, mock_delete): + """jira_delete must not be called if issues exist but all have 0 worklogs.""" + from api.jira.report_worklogs import jira_worklogs + + mock_client = mock_jira.return_value + mock_client.filter_sv_parent_in_board.return_value = [ + {"key": "QATT-1", "fields": {"summary": "Test issue", "parent": None}} + ] + mock_client.filter_child_issues.return_value = [] + mock_client.filter_worklogs.return_value = [] + + with self.assertRaises(ValueError) as ctx: + jira_worklogs() + + self.assertIn("no worklog data found", str(ctx.exception)) + mock_delete.assert_not_called() + + +class TestJiraQARequestsEmptyPayload(unittest.TestCase): + + @patch("api.jira.report_qa_requests.jira_delete") + @patch("api.jira.report_qa_requests._jira") + def test_qa_requests_raises_on_empty_payload(self, mock_jira, mock_delete): + """jira_delete must not be called if filters() returns no issues.""" + from api.jira.report_qa_requests import jira_qa_requests + + mock_jira.return_value.filters.return_value = [] + + with self.assertRaises(ValueError) as ctx: + jira_qa_requests() + + self.assertIn("empty payload", str(ctx.exception)) + mock_delete.assert_not_called() + + @patch("api.jira.report_qa_requests.jira_delete") + @patch("api.jira.report_qa_requests._jira") + def test_qa_requests_workload_raises_on_empty_payload(self, mock_jira, mock_delete): + """jira_delete must not be called if filters_new_issue_type() returns empty.""" + from api.jira.report_qa_requests import jira_qa_requests_workload + + mock_jira.return_value.filters_new_issue_type.return_value = [] + + with self.assertRaises(ValueError) as ctx: + jira_qa_requests_workload() + + self.assertIn("empty payload", str(ctx.exception)) + mock_delete.assert_not_called() + + @patch("api.jira.report_qa_requests_desktop.jira_delete") + @patch("api.jira.report_qa_requests_desktop._jira") + def test_qa_requests_desktop_raises_on_empty_payload(self, mock_jira, mock_delete): + """jira_delete must not be called if filters() returns no issues.""" + from api.jira.report_qa_requests_desktop import jira_qa_requests_desktop + + mock_jira.return_value.filters.return_value = [] + + with self.assertRaises(ValueError) as ctx: + jira_qa_requests_desktop() + + self.assertIn("empty payload", str(ctx.exception)) + mock_delete.assert_not_called()