diff --git a/manager/api/tests/test_jira.py b/manager/api/tests/test_jira.py index f90c77d0..4afd4eef 100644 --- a/manager/api/tests/test_jira.py +++ b/manager/api/tests/test_jira.py @@ -22,6 +22,7 @@ import os import random from unittest.mock import patch +from urllib.parse import quote import pytest import requests @@ -78,7 +79,6 @@ class JiraTestCase(TestCase): def setUp(self): """Define the test suite setup.""" - # Arrange shared_params = [ "lfa_files_urls", "message_text", @@ -261,6 +261,12 @@ def setUp(self): self.jira_request_narrative_full_jira_comment.user = "user" self.jira_request_narrative_full_jira_comment.get_host = lambda: "localhost" + # headers for jira requests + self.headers = { + "Authorization": f"Basic {os.environ.get('JIRA_API_TOKEN')}", + "content-type": "application/json", + } + def test_missing_parameters(self): """Test call to jira_ticket function with missing parameters""" @@ -493,11 +499,36 @@ def test_handle_exposure_jira_payload(self): def test_get_jira_obs_report(self): """Test call to get_jira_obs_report function with all needed parameters""" + + # Arrange mock_jira_patcher = patch("requests.get") mock_jira_client = mock_jira_patcher.start() - response = requests.Response() - response.status_code = 200 - response.json = lambda: { + + url_call_1 = ( + f"https://{os.environ.get('JIRA_API_HOSTNAME')}/rest/api/latest/myself" + ) + + response_1 = requests.Response() + response_1.status_code = 200 + response_1.json = lambda: { + "timeZone": "America/Phoenix", + } + + # American/Phoenix timezone is UTC-7 + day_obs = "20241127" + jql_query = ( + "project = 'OBS' " + "AND created >= '2024-11-27 05:00' " + "AND created <= '2024-11-28 05:00'" + ) + url_call_2 = ( + f"https://{os.environ.get('JIRA_API_HOSTNAME')}" + f"/rest/api/latest/search?jql={quote(jql_query)}" + ) + + response_2 = requests.Response() + response_2.status_code = 200 + response_2.json = lambda: { "issues": [ { "key": "LOVE-XX", @@ -505,38 +536,75 @@ def test_get_jira_obs_report(self): "summary": "Issue title", TIME_LOST_FIELD: 13.6, "creator": {"displayName": "user"}, - "created": "2022-07-03T19:58:13.00000", + "created": "2024-11-27T12:00:00.00000", }, } ] } - mock_jira_client.return_value = response + mock_jira_client.side_effect = [response_1, response_2] + + # Act request_data = { - "day_obs": 20240902, + "day_obs": day_obs, } jira_response = get_jira_obs_report(request_data) + + # Assert + mock_jira_client.assert_any_call(url_call_1, headers=self.headers) + mock_jira_client.assert_any_call(url_call_2, headers=self.headers) + assert jira_response[0]["key"] == "LOVE-XX" assert jira_response[0]["summary"] == "Issue title" assert jira_response[0]["time_lost"] == 13.6 assert jira_response[0]["reporter"] == "user" - assert jira_response[0]["created"] == "2022-07-03T19:58:13" + assert jira_response[0]["created"] == "2024-11-27T12:00:00" mock_jira_patcher.stop() def test_get_jira_obs_report_fail(self): """Test call to get_jira_obs_report function with fail response""" + + # Arrange + request_data = { + "day_obs": 20241127, + } + mock_jira_patcher = patch("requests.get") mock_jira_client = mock_jira_patcher.start() - response = requests.Response() - response.status_code = 400 - mock_jira_client.return_value = response - request_data = { - "day_obs": 20240902, + success_response_1 = requests.Response() + success_response_1.status_code = 200 + success_response_1.json = lambda: { + "timeZone": "America/Phoenix", } - with pytest.raises(Exception): + + failed_response = requests.Response() + failed_response.status_code = 400 + + # Act + # Incomplete request data + incomplete_request_data = {} + with self.assertRaises(ValueError): + get_jira_obs_report(incomplete_request_data) + + # Fail response from Jira to get user data + mock_jira_client.return_value = failed_response + with pytest.raises(Exception) as e: + get_jira_obs_report(request_data) + assert ( + str(e.value) + == f"Error getting user timezone from {os.environ.get('JIRA_API_HOSTNAME')}" + ) + + # Fail response from Jira to get issues + mock_jira_client.side_effect = [success_response_1, failed_response] + with pytest.raises(Exception) as e: get_jira_obs_report(request_data) + assert ( + str(e.value) + == f"Error getting issues from {os.environ.get('JIRA_API_HOSTNAME')}" + ) mock_jira_patcher.stop() diff --git a/manager/manager/utils.py b/manager/manager/utils.py index 1bbc377a..fc7ce50b 100644 --- a/manager/manager/utils.py +++ b/manager/manager/utils.py @@ -34,6 +34,7 @@ from astropy.units import hour from django.conf import settings from django.core.files.storage import Storage +from pytz import timezone from rest_framework.permissions import BasePermission from rest_framework.response import Response @@ -652,28 +653,72 @@ def handle_jira_payload(request, lfa_urls=[]): def get_jira_obs_report(request_data): - """Query all issues of the OBS project for a certain day. - Then get the total observation time loss from the time_lost param - """ + """Connect to the Rubin Observatory JIRA Cloud REST API to + query all issues of the OBS project for a certain obs day. + + For more information on the REST API endpoints refer to: + - https://developer.atlassian.com/cloud/jira/platform/rest/v3 + - https://developer.atlassian.com/cloud/jira/platform/\ + basic-auth-for-rest-apis/ + + Parameters + ---------- + request_data : `dict` + The request data + + Notes + ----- + The JIRA REST API query is based on the user timezone so + we need to account for the timezone difference between the user and the + server. The user timezone is obtained from the JIRA API. + Returns + ------- + List + List of dictionaries containing the following keys: + - key: The issue key + - summary: The issue summary + - time_lost: The time lost in the issue + - reporter: The issue reporter + - created: The issue creation date + """ intitial_day_obs_tai = get_obsday_to_tai(request_data.get("day_obs")) final_day_obs_tai = intitial_day_obs_tai + timedelta(days=1) - initial_day_obs_string = intitial_day_obs_tai.strftime("%Y-%m-%d") - final_day_obs_string = final_day_obs_tai.strftime("%Y-%m-%d") + headers = { + "Authorization": f"Basic {os.environ.get('JIRA_API_TOKEN')}", + "content-type": "application/json", + } + + # Get user timezone + url = f"https://{os.environ.get('JIRA_API_HOSTNAME')}/rest/api/latest/myself" + response = requests.get(url, headers=headers) + if response.status_code == 200: + user_timezone = timezone(response.json()["timeZone"]) + else: + raise Exception( + f"Error getting user timezone from {os.environ.get('JIRA_API_HOSTNAME')}" + ) + + start_date_user_datetime = intitial_day_obs_tai.replace( + tzinfo=timezone("UTC") + ).astimezone(user_timezone) + end_date_user_datetime = final_day_obs_tai.replace( + tzinfo=timezone("UTC") + ).astimezone(user_timezone) + + initial_day_obs_string = start_date_user_datetime.strftime("%Y-%m-%d") + final_day_obs_string = end_date_user_datetime.strftime("%Y-%m-%d") + start_date_user_time_string = start_date_user_datetime.time().strftime("%H:%M") + end_date_user_time_string = end_date_user_datetime.time().strftime("%H:%M") # JQL query to find issues created on a specific date jql_query = ( f"project = 'OBS' " - f"AND created >= '{initial_day_obs_string} 12:00' " - f"AND created <= '{final_day_obs_string} 12:00'" + f"AND created >= '{initial_day_obs_string} {start_date_user_time_string}' " + f"AND created <= '{final_day_obs_string} {end_date_user_time_string}'" ) - headers = { - "Authorization": f"Basic {os.environ.get('JIRA_API_TOKEN')}", - "content-type": "application/json", - } - url = f"https://{os.environ.get('JIRA_API_HOSTNAME')}/rest/api/latest/search?jql={quote(jql_query)}" response = requests.get(url, headers=headers) if response.status_code == 200: