-
-
Notifications
You must be signed in to change notification settings - Fork 637
Refactor connector testing framework phase 1 #3663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 9 commits
987765d
6715f82
d0b5ea7
8419ace
4abcb35
38ebe10
564be19
c3df418
ecd50da
a2fb01c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,13 +42,18 @@ def run(self) -> dict: | |
| parameter=Parameter.objects.get(name="url_key_name", python_module=pm), | ||
| connector_config=cc, | ||
| ) | ||
| with patch("requests.head"): | ||
| from unittest.mock import MagicMock | ||
|
|
||
| mock_response = MagicMock() | ||
| mock_response.status_code = 200 | ||
|
|
||
| with patch("requests.head", return_value=mock_response): | ||
| result = MockUpConnector(cc).health_check(self.user) | ||
| self.assertTrue(result) | ||
| cc.disabled = False | ||
| cc.save() | ||
| result = MockUpConnector(cc).health_check(self.user) | ||
| self.assertTrue(result) | ||
| self.assertTrue(result) | ||
| cc.disabled = False | ||
| cc.save() | ||
| result = MockUpConnector(cc).health_check(self.user) | ||
| self.assertTrue(result) | ||
| cc.delete() | ||
| pc.delete() | ||
|
|
||
|
|
@@ -132,6 +137,15 @@ def handler(signum, frame): | |
| signal.alarm(timeout_seconds) | ||
| try: | ||
| sub.start(job.pk, {}, uuid()) | ||
| except TypeError as e: | ||
| # Skip only known parameter-validation TypeErrors raised during | ||
| # configuration loading (e.g. missing required params). | ||
| # Re-raise anything else so real connector bugs are not masked. | ||
| if "does not have a valid value" not in str(e): | ||
| self.fail( | ||
| f"Connector {subclass.__name__} with config {config.name} " | ||
| f"raised unexpected TypeError: {e}" | ||
| ) | ||
|
Comment on lines
+140
to
+148
|
||
| except Exception as e: | ||
| self.fail(f"Connector {subclass.__name__} with config {config.name} failed {e}") | ||
| finally: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| # This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl | ||
| # See the file 'LICENSE' for copying permission. | ||
|
|
||
| from contextlib import ExitStack | ||
|
|
||
| from django.contrib.auth import get_user_model | ||
| from django.test import TestCase | ||
|
|
||
| from api_app.analyzables_manager.models import Analyzable | ||
| from api_app.choices import Classification | ||
| from api_app.connectors_manager.models import ConnectorConfig, ConnectorReport | ||
| from api_app.models import Job, Parameter, PluginConfig | ||
|
|
||
|
|
||
| class BaseConnectorTest(TestCase): | ||
| connector_class = None | ||
| fixtures = ["api_app/fixtures/0001_user.json"] | ||
|
|
||
| def setUp(self): | ||
| super().setUp() | ||
| User = get_user_model() | ||
| self.superuser = User.objects.get(is_superuser=True) | ||
|
|
||
| def _setup_connector( | ||
| self, | ||
| connector_class_name, | ||
| observable_name="8.8.8.8", | ||
| observable_type=Classification.IP, | ||
| params=None, | ||
| ): | ||
| """ | ||
| Setup a connector with required side-effects (Job, Analyzable, etc.) | ||
| Returns (connector, job, config, analyzable) so callers can clean up | ||
| using a direct reference to the created Analyzable instance. | ||
| """ | ||
| config = ConnectorConfig.objects.get(python_module__module__endswith=f".{connector_class_name}") | ||
|
|
||
| # Use update_or_create so repeated calls with different param values | ||
| # always reflect the intended configuration rather than leaving stale rows. | ||
| if params: | ||
| for name, value in params.items(): | ||
| param = Parameter.objects.get(python_module=config.python_module, name=name) | ||
| PluginConfig.objects.update_or_create( | ||
| parameter=param, | ||
| connector_config=config, | ||
| defaults={"value": value, "for_organization": False, "owner": None}, | ||
| ) | ||
|
|
||
| # Keep a direct reference so the finally block can delete the exact row | ||
| # rather than re-querying by name (which is not unique). | ||
| analyzable = Analyzable.objects.create(name=observable_name, classification=observable_type) | ||
| job = Job.objects.create( | ||
| analyzable=analyzable, | ||
| user=self.superuser, | ||
| status=Job.STATUSES.REPORTED_WITHOUT_FAILS.value, | ||
| ) | ||
| job.connectors_to_execute.set([config]) | ||
|
|
||
| connector = self.connector_class(config) | ||
| connector.job_id = job.pk | ||
|
|
||
| return connector, job, config, analyzable | ||
|
|
||
| def get_mocked_response(self): | ||
| """ | ||
| Subclasses should override this to provide a list of patches. | ||
|
PranavShukla7 marked this conversation as resolved.
Outdated
|
||
| """ | ||
| return [] | ||
|
|
||
| def _apply_patches(self, patches): | ||
| if not patches: | ||
| return ExitStack() | ||
|
|
||
| stack = ExitStack() | ||
| if isinstance(patches, (list, tuple)): | ||
| for p in patches: | ||
| stack.enter_context(p) | ||
| else: | ||
| stack.enter_context(patches) | ||
| return stack | ||
|
|
||
| def execute_run_logic( | ||
| self, | ||
| connector_class_name, | ||
| observable_name="8.8.8.8", | ||
| observable_type=Classification.IP, | ||
| params=None, | ||
| ): | ||
| """ | ||
| Generic test runner for connectors. | ||
| Exceptions are allowed to propagate so that the full traceback is | ||
| visible in the test output and assertion failures are not swallowed. | ||
| """ | ||
| if not self.connector_class: | ||
| self.skipTest("connector_class not set") | ||
|
|
||
| connector, job, config, analyzable = self._setup_connector( | ||
| connector_class_name, observable_name, observable_type, params | ||
| ) | ||
|
|
||
| patches = self.get_mocked_response() | ||
| try: | ||
| with self._apply_patches(patches): | ||
| from kombu import uuid | ||
|
|
||
| connector.report = config.generate_empty_report( | ||
| job, str(uuid()), ConnectorReport.STATUSES.RUNNING.value | ||
| ) | ||
| connector.config(params or {}) | ||
| connector.before_run() | ||
| response = connector.run() | ||
|
|
||
| self.assertIsInstance(response, (dict, list)) | ||
| return response | ||
| finally: | ||
| # Delete using the direct reference captured at creation time to | ||
| # avoid accidentally removing an unrelated Analyzable that shares | ||
| # the same name. | ||
| job.delete() | ||
| analyzable.delete() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl | ||
| # See the file 'LICENSE' for copying permission. | ||
|
|
||
| from unittest.mock import patch | ||
|
|
||
| from api_app.choices import Classification | ||
| from api_app.connectors_manager.connectors.email_sender import EmailSender | ||
|
|
||
| from .base_test_class import BaseConnectorTest | ||
|
PranavShukla7 marked this conversation as resolved.
|
||
|
|
||
|
Comment on lines
+8
to
+10
|
||
|
|
||
| class EmailSenderTestCase(BaseConnectorTest): | ||
| connector_class = EmailSender | ||
|
|
||
| def get_mocked_response(self): | ||
| return patch("django.core.mail.EmailMessage.send", return_value="Email sent") | ||
|
|
||
|
PranavShukla7 marked this conversation as resolved.
|
||
| def test_email_sender_run(self): | ||
| params = { | ||
| "subject": "Test Issue", | ||
| "body": "Test body", | ||
| } | ||
| res = self.execute_run_logic( | ||
| "EmailSender", | ||
| observable_name="test@example.com", | ||
| observable_type=Classification.GENERIC, | ||
| params=params, | ||
| ) | ||
| self.assertIn("subject", res) | ||
| self.assertEqual(res["to"], ["test@example.com"]) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # This file is a part of IntelOwl https://github.com/intelowlproject/IntelOwl | ||
| # See the file 'LICENSE' for copying permission. | ||
|
|
||
| from unittest.mock import patch | ||
|
|
||
| from api_app.choices import Classification | ||
| from api_app.connectors_manager.connectors.slack import Slack | ||
|
|
||
| from .base_test_class import BaseConnectorTest | ||
|
PranavShukla7 marked this conversation as resolved.
|
||
|
|
||
|
Comment on lines
+8
to
+10
|
||
|
|
||
| class MockClient: | ||
| def __init__(self, *args, **kwargs): | ||
| pass | ||
|
|
||
| def chat_postMessage(self, *args, **kwargs): | ||
| pass | ||
|
|
||
|
|
||
| class SlackTestCase(BaseConnectorTest): | ||
| connector_class = Slack | ||
|
|
||
| def get_mocked_response(self): | ||
| return patch("slack_sdk.WebClient", side_effect=MockClient) | ||
|
|
||
| def test_slack_run(self): | ||
| params = { | ||
| "channel": "#general", | ||
| "slack_username": "intelowl", | ||
| "token": "xoxb-123", | ||
| } | ||
| res = self.execute_run_logic( | ||
| "Slack", | ||
| observable_name="8.8.8.8", | ||
| observable_type=Classification.IP, | ||
| params=params, | ||
| ) | ||
| self.assertEqual(res, {}) | ||
Uh oh!
There was an error while loading. Please reload this page.