Skip to content

Commit b10e232

Browse files
committed
add unit tests
1 parent f1ab221 commit b10e232

File tree

5 files changed

+203
-41
lines changed

5 files changed

+203
-41
lines changed

src/aap_eda/api/views/event_stream.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from aap_eda.api import exceptions as api_exc, filters, serializers
3232
from aap_eda.core import models
3333
from aap_eda.core.enums import EventStreamAuthType, ResourceType
34-
from aap_eda.core.exceptions import GatewayAPIError, MissingCredentials
34+
from aap_eda.core.exceptions import GatewayAPIError, MissingCredentialsError
3535
from aap_eda.core.utils import logging_utils
3636
from aap_eda.services.sync_certs import SyncCertificates
3737

@@ -327,5 +327,5 @@ def _sync_certificates(
327327
obj.delete(event_stream.id)
328328
else:
329329
obj.update()
330-
except (GatewayAPIError, MissingCredentials) as ex:
331-
logger.error("Could not %s certificates %s", action, str(ex))
330+
except (GatewayAPIError, MissingCredentialsError) as ex:
331+
logger.error("Could not %s certificates: %s", action, str(ex))

src/aap_eda/core/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,5 @@ class GatewayAPIError(Exception):
5353
pass
5454

5555

56-
class MissingCredentials(Exception):
56+
class MissingCredentialsError(Exception):
5757
pass

src/aap_eda/services/signals.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Signal handlers for EDA services."""
2+
from django.db.models.signals import post_save
3+
from django.dispatch import receiver
4+
5+
from aap_eda.core import enums, models
6+
from aap_eda.core.exceptions import GatewayAPIError, MissingCredentialsError
7+
from aap_eda.services.sync_certs import SyncCertificates
8+
9+
10+
@receiver(post_save, sender=models.EdaCredential)
11+
def gw_handler(
12+
sender: type[models.EdaCredential],
13+
instance: models.EdaCredential,
14+
**kwargs,
15+
) -> None:
16+
"""Handle updates to EdaCredential object and force a certificate sync."""
17+
if (
18+
instance.credential_type is not None
19+
and instance.credential_type.name
20+
== enums.EventStreamCredentialType.MTLS
21+
and hasattr(instance, "_request")
22+
):
23+
try:
24+
objects = models.EventStream.objects.filter(
25+
eda_credential_id=instance.id
26+
)
27+
if len(objects) > 0:
28+
SyncCertificates(instance.id).update()
29+
except (GatewayAPIError, MissingCredentialsError) as ex:
30+
from aap_eda.services import sync_certs
31+
32+
sync_certs.LOGGER.error(
33+
"Couldn't trigger gateway certificate updates %s", str(ex)
34+
)

src/aap_eda/services/sync_certs.py

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@
2121
import yaml
2222
from ansible_base.resource_registry import resource_server
2323
from django.conf import settings
24-
from django.db.models.signals import post_save
25-
from django.dispatch import receiver
2624
from rest_framework import status
2725

28-
from aap_eda.core import enums, models
29-
from aap_eda.core.exceptions import GatewayAPIError, MissingCredentials
26+
from aap_eda.core import models
27+
from aap_eda.core.exceptions import GatewayAPIError, MissingCredentialsError
3028

3129
LOGGER = logging.getLogger(__name__)
3230
SLUG = "api/gateway/v1/ca_certificates/"
@@ -48,7 +46,7 @@ def __init__(self, eda_credential_id: int):
4846
id=self.eda_credential_id
4947
)
5048

51-
def update(self):
49+
def update(self) -> None:
5250
"""Handle creating and updating the certificate in Gateway."""
5351
inputs = yaml.safe_load(self.eda_credential.inputs.get_secret_value())
5452
existing_object = self._fetch_from_gateway()
@@ -98,16 +96,22 @@ def update(self):
9896
status.HTTP_200_OK,
9997
status.HTTP_201_CREATED,
10098
]:
101-
LOGGER.debug("Certificate updated")
99+
LOGGER.debug("Certificate updated successfully")
102100
elif response.status_code == status.HTTP_400_BAD_REQUEST:
103101
LOGGER.error("Update failed")
104102
else:
105-
LOGGER.error("Couldn't update certificate")
103+
LOGGER.error(
104+
"Couldn't update certificate. "
105+
"Status code: %s, Response: %s",
106+
response.status_code,
107+
response.text,
108+
)
109+
raise GatewayAPIError
106110

107111
else:
108112
LOGGER.debug("No changes detected")
109113

110-
def delete(self, event_stream_id: Optional[int]):
114+
def delete(self, event_stream_id: Optional[int] = None) -> None:
111115
"""Delete the Certificate from Gateway."""
112116
existing_object = self._fetch_from_gateway()
113117
if not existing_object:
@@ -121,7 +125,7 @@ def delete(self, event_stream_id: Optional[int]):
121125
elif len(objects) == 1 and event_stream_id == objects[0].id:
122126
self._delete_from_gateway(existing_object)
123127

124-
def _delete_from_gateway(self, existing_object: dict):
128+
def _delete_from_gateway(self, existing_object: dict) -> None:
125129
slug = f"{SLUG}/{existing_object['id']}/"
126130
url = urljoin(self.gateway_url, slug)
127131
headers = self._prep_headers()
@@ -132,14 +136,19 @@ def _delete_from_gateway(self, existing_object: dict):
132136
timeout=DEFAULT_TIMEOUT,
133137
)
134138
if response.status_code == status.HTTP_200_OK:
135-
LOGGER.debug("Certificate object deleted")
136-
if response.status_code == status.HTTP_404_NOT_FOUND:
139+
LOGGER.debug("Certificate object deleted successfully")
140+
elif response.status_code == status.HTTP_404_NOT_FOUND:
137141
LOGGER.warning("Certificate object missing during delete")
138142
else:
139-
LOGGER.error("Couldn't delete certificate object in gateway")
143+
LOGGER.error(
144+
"Couldn't delete certificate object. "
145+
"Status code: %s, Response: %s",
146+
response.status_code,
147+
response.text,
148+
)
140149
raise GatewayAPIError
141150

142-
def _fetch_from_gateway(self):
151+
def _fetch_from_gateway(self) -> dict:
143152
slug = f"{SLUG}/?remote_id={self._get_remote_id()}"
144153
url = urljoin(self.gateway_url, slug)
145154
headers = self._prep_headers()
@@ -161,37 +170,20 @@ def _fetch_from_gateway(self):
161170
LOGGER.debug("Certificate object does not exist in gateway")
162171
return {}
163172

164-
LOGGER.error("Error fetching certificate object")
173+
LOGGER.error(
174+
"Error fetching certificate object. Status code: %s, Response: %s",
175+
response.status_code,
176+
response.text,
177+
)
165178
raise GatewayAPIError
166179

167180
def _get_remote_id(self) -> str:
168181
return f"eda_{self.eda_credential_id}"
169182

170-
def _prep_headers(self) -> dict:
183+
def _prep_headers(self) -> dict[str, str]:
171184
token = resource_server.get_service_token()
172185
if token:
173186
return {SERVICE_TOKEN_HEADER: token}
174187

175188
LOGGER.error("Cannot connect to gateway service token")
176-
raise MissingCredentials
177-
178-
179-
@receiver(post_save, sender=models.EdaCredential)
180-
def gw_handler(sender, instance, **kwargs):
181-
"""Handle updates to EdaCredential object and force a certificate sync."""
182-
if (
183-
instance.credential_type is not None
184-
and instance.credential_type.name
185-
== enums.EventStreamCredentialType.MTLS
186-
and hasattr(instance, "_request")
187-
):
188-
try:
189-
objects = models.EventStream.objects.filter(
190-
eda_credential_id=instance.id
191-
)
192-
if len(objects) > 0:
193-
SyncCertificates(instance.id).update()
194-
except (GatewayAPIError, MissingCredentials) as ex:
195-
LOGGER.error(
196-
"Couldn't trigger gateway certificate updates %s", str(ex)
197-
)
189+
raise MissingCredentialsError
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
import pytest
4+
from django.conf import settings
5+
from rest_framework import status
6+
7+
from aap_eda.core import models
8+
from aap_eda.core.exceptions import GatewayAPIError, MissingCredentialsError
9+
from aap_eda.services.sync_certs import SyncCertificates
10+
11+
12+
@pytest.fixture
13+
def mock_eda_credential():
14+
credential = MagicMock(spec=models.EdaCredential)
15+
credential.id = 1
16+
credential.name = "test-credential"
17+
credential.inputs.get_secret_value.return_value = "certificate: test-cert"
18+
return credential
19+
20+
21+
@pytest.fixture
22+
def sync_certs(mock_eda_credential):
23+
with patch("aap_eda.core.models.EdaCredential.objects.get") as mock_get:
24+
mock_get.return_value = mock_eda_credential
25+
return SyncCertificates(eda_credential_id=1)
26+
27+
28+
def test_init(sync_certs, mock_eda_credential):
29+
assert sync_certs.eda_credential_id == 1
30+
assert sync_certs.eda_credential == mock_eda_credential
31+
assert sync_certs.gateway_url == settings.RESOURCE_SERVER["URL"]
32+
33+
34+
@patch("aap_eda.services.sync_certs.requests.patch")
35+
@patch("aap_eda.services.sync_certs.SyncCertificates._fetch_from_gateway")
36+
@patch("aap_eda.services.sync_certs.SyncCertificates._prep_headers")
37+
def test_update_existing_cert(
38+
mock_prep_headers, mock_fetch, mock_patch, sync_certs
39+
):
40+
mock_fetch.return_value = {"id": 1, "sha256": "old-hash"}
41+
mock_prep_headers.return_value = {"X-ANSIBLE-SERVICE-AUTH": "token"}
42+
mock_response = MagicMock()
43+
mock_response.status_code = status.HTTP_200_OK
44+
mock_patch.return_value = mock_response
45+
46+
sync_certs.update()
47+
mock_patch.assert_called_once()
48+
49+
50+
@patch("aap_eda.services.sync_certs.requests.post")
51+
@patch("aap_eda.services.sync_certs.SyncCertificates._fetch_from_gateway")
52+
@patch("aap_eda.services.sync_certs.SyncCertificates._prep_headers")
53+
def test_update_new_cert(mock_prep_headers, mock_fetch, mock_post, sync_certs):
54+
mock_fetch.return_value = {}
55+
mock_prep_headers.return_value = {"X-ANSIBLE-SERVICE-AUTH": "token"}
56+
mock_response = MagicMock()
57+
mock_response.status_code = status.HTTP_201_CREATED
58+
mock_post.return_value = mock_response
59+
60+
sync_certs.update()
61+
mock_post.assert_called_once()
62+
63+
64+
@patch("aap_eda.services.sync_certs.requests.delete")
65+
@patch("aap_eda.services.sync_certs.SyncCertificates._fetch_from_gateway")
66+
@patch("aap_eda.services.sync_certs.SyncCertificates._prep_headers")
67+
@patch("aap_eda.core.models.EdaCredential.objects.get")
68+
@patch("aap_eda.core.models.EventStream.objects.filter")
69+
def test_delete(
70+
mock_filter,
71+
mock_get,
72+
mock_prep_headers,
73+
mock_fetch,
74+
mock_delete,
75+
sync_certs,
76+
mock_eda_credential,
77+
):
78+
mock_get.return_value = mock_eda_credential
79+
mock_fetch.return_value = {"id": 1}
80+
mock_prep_headers.return_value = {"X-ANSIBLE-SERVICE-AUTH": "token"}
81+
mock_response = MagicMock()
82+
mock_response.status_code = status.HTTP_200_OK
83+
mock_delete.return_value = mock_response
84+
mock_filter.return_value = MagicMock()
85+
mock_filter.return_value.__len__.return_value = 0
86+
87+
sync_certs.delete()
88+
mock_delete.assert_called_once()
89+
90+
91+
@patch("aap_eda.services.sync_certs.requests.get")
92+
@patch("aap_eda.services.sync_certs.SyncCertificates._prep_headers")
93+
def test_fetch_from_gateway(mock_prep_headers, mock_get, sync_certs):
94+
mock_prep_headers.return_value = {"X-ANSIBLE-SERVICE-AUTH": "token"}
95+
mock_response = MagicMock()
96+
mock_response.status_code = status.HTTP_200_OK
97+
mock_response.json.return_value = {"count": 1, "results": [{"id": 1}]}
98+
mock_get.return_value = mock_response
99+
100+
result = sync_certs._fetch_from_gateway()
101+
assert result == {"id": 1}
102+
103+
104+
def test_get_remote_id(sync_certs):
105+
assert sync_certs._get_remote_id() == "eda_1"
106+
107+
108+
@patch("aap_eda.services.sync_certs.resource_server.get_service_token")
109+
def test_prep_headers(mock_get_token, sync_certs):
110+
mock_get_token.return_value = "test-token"
111+
headers = sync_certs._prep_headers()
112+
assert headers == {"X-ANSIBLE-SERVICE-AUTH": "test-token"}
113+
114+
115+
@patch("aap_eda.services.sync_certs.resource_server.get_service_token")
116+
def test_prep_headers_missing_credentials(mock_get_token, sync_certs):
117+
mock_get_token.return_value = None
118+
with pytest.raises(MissingCredentialsError):
119+
sync_certs._prep_headers()
120+
121+
122+
@patch("aap_eda.services.sync_certs.requests.patch")
123+
@patch("aap_eda.services.sync_certs.SyncCertificates._fetch_from_gateway")
124+
@patch("aap_eda.services.sync_certs.SyncCertificates._prep_headers")
125+
def test_update_error_handling(
126+
mock_prep_headers, mock_fetch, mock_patch, sync_certs
127+
):
128+
mock_fetch.return_value = {"id": 1, "sha256": "old-hash"}
129+
mock_prep_headers.return_value = {"X-ANSIBLE-SERVICE-AUTH": "token"}
130+
mock_response = MagicMock()
131+
mock_response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
132+
mock_response.text = "Server error"
133+
mock_patch.return_value = mock_response
134+
135+
with pytest.raises(GatewayAPIError):
136+
sync_certs.update()

0 commit comments

Comments
 (0)