Coverage for src/pytest_xray/xray_publisher.py: 91.5%
80 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 14:00 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 14:00 +0000
1import json
2from typing import Any, Callable, Optional, Union
4import requests
5from requests import PreparedRequest
6from requests.auth import AuthBase
8from pytest_xray.constant import AUTHENTICATE_ENDPOINT
9from pytest_xray.exceptions import XrayError
11AuthType = Optional[Union[tuple[str, str], AuthBase, Callable[[PreparedRequest], PreparedRequest]]]
14# Connect timeout and read inactivity timeout, not a total wall-clock deadline.
15REQUEST_TIMEOUT = (10, 60)
18class ClientSecretAuth(AuthBase):
19 """Bearer authentication with Client ID and a Client Secret."""
21 def __init__(self, base_url: str, client_id: str, client_secret: str, verify: Union[bool, str] = True) -> None:
22 if base_url.endswith('/'): 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true
23 base_url = base_url[:-1]
24 self.base_url = base_url
25 self.client_id = client_id
26 self.client_secret = client_secret
27 self.verify = verify
29 @property
30 def endpoint_url(self) -> str:
31 return f'{self.base_url}{AUTHENTICATE_ENDPOINT}'
33 def __call__(self, r: requests.PreparedRequest) -> requests.PreparedRequest:
34 headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
35 auth_data = {'client_id': self.client_id, 'client_secret': self.client_secret}
37 try:
38 response = requests.post(
39 self.endpoint_url,
40 data=json.dumps(auth_data),
41 headers=headers,
42 verify=self.verify,
43 timeout=REQUEST_TIMEOUT,
44 allow_redirects=False,
45 )
46 response.raise_for_status()
47 if response.status_code != 200:
48 raise XrayError('Authentication did not return HTTP 200')
49 auth_token = response.json()
50 if not isinstance(auth_token, str) or not auth_token.strip() or any(c.isspace() for c in auth_token):
51 raise XrayError('Authentication returned an invalid token')
52 except requests.exceptions.RequestException as exc:
53 raise XrayError(f'{type(exc).__name__}: cannot authenticate with Xray') from exc
54 except ValueError as exc:
55 raise XrayError('Authentication returned invalid JSON') from exc
56 r.headers['Authorization'] = f'Bearer {auth_token}'
57 return r
60class ApiKeyAuth(AuthBase):
61 """Personal access token authentication."""
63 def __init__(self, api_key: str) -> None:
64 self.api_key = api_key
66 def __call__(self, r: requests.PreparedRequest) -> requests.PreparedRequest:
67 r.headers['Authorization'] = f'Bearer {self.api_key}'
68 return r
71class XrayPublisher:
72 """Exports Xray report to a Jira server."""
74 def __init__(self, base_url: str, endpoint: str, auth: AuthType, verify: Union[bool, str] = True) -> None:
75 if base_url.endswith('/'): 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true
76 base_url = base_url[:-1]
77 self.base_url = base_url
78 self.endpoint = endpoint
79 self.auth = auth
80 self.verify = verify
82 @property
83 def endpoint_url(self) -> str:
84 """Return full URL to the server."""
85 return self.base_url + self.endpoint
87 def _send_data(self, url: str, auth: AuthType, data: dict[str, Any]) -> dict[str, Any]:
88 headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
89 try:
90 response = requests.request(
91 method='POST',
92 url=url,
93 headers=headers,
94 json=data,
95 auth=auth,
96 verify=self.verify,
97 timeout=REQUEST_TIMEOUT,
98 allow_redirects=False,
99 )
100 response.raise_for_status()
101 if not 200 <= response.status_code < 300: 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true
102 raise XrayError(f'Xray import returned unexpected HTTP {response.status_code}; redirects are disabled')
103 result = response.json()
104 except requests.exceptions.HTTPError as exc:
105 raise XrayError(f'HTTPError: Could not post to Xray. Response status code: {response.status_code}') from exc
106 except requests.exceptions.RequestException as exc:
107 raise XrayError(f'{type(exc).__name__}: cannot post results to Xray') from exc
108 except ValueError as exc:
109 raise XrayError('Xray import returned invalid JSON') from exc
110 if not isinstance(result, dict):
111 raise XrayError('Xray import response must be a JSON object')
112 return result
114 def publish(self, data: dict[str, Any]) -> str:
115 """Publish once; never retry a potentially accepted import automatically."""
116 response_data = self._send_data(self.endpoint_url, self.auth, data)
117 try:
118 key = response_data['testExecIssue']['key'] if 'testExecIssue' in response_data else response_data['key']
119 except (KeyError, TypeError):
120 raise XrayError('Cannot read Test Execution ID from server response') from None
121 if not isinstance(key, str) or not key.strip():
122 raise XrayError('Cannot read Test Execution ID from server response')
123 return key