Skip to content
Open
13 changes: 0 additions & 13 deletions api_app/connectors_manager/connectors/email_sender.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from api_app.connectors_manager.classes import Connector
from intel_owl.settings import DEFAULT_FROM_EMAIL
from tests.mock_utils import if_mock_connections, patch


class EmailSender(Connector):
Expand Down Expand Up @@ -44,15 +43,3 @@ def run(self) -> dict:

def update(self) -> bool:
pass

@classmethod
def _monkeypatch(cls):
patches = [
if_mock_connections(
patch(
"django.core.mail.EmailMessage.send",
return_value="Email sent",
)
)
]
return super()._monkeypatch(patches=patches)
35 changes: 0 additions & 35 deletions api_app/connectors_manager/connectors/opencti.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,38 +226,3 @@ def run(self):
except Exception:
pass
raise

@classmethod
def _monkeypatch(cls):
"""Install pycti stubs when connection mocking is enabled."""
if not getattr(settings, "MOCK_CONNECTIONS", False):
return

def _configure(start_fn):
def inner(self, job_id, runtime_configuration, task_id, *args, **kwargs):
# Avoid real OpenCTI network calls
pycti.OpenCTIApiClient = lambda *a, **k: None

def _fake_create(*_args, **_kwargs):
return {"id": 1}

def _noop(*_args, **_kwargs):
return None

# Ensure core entities always return a dict with an id in CI generic tests.
pycti.Identity.create = _fake_create
pycti.MarkingDefinition.create = _fake_create
pycti.StixCyberObservable.create = _fake_create
pycti.Label.create = _fake_create
pycti.Report.create = _fake_create
pycti.ExternalReference.create = _fake_create

# No-op the linking methods that would otherwise dereference opencti/app_logger.
pycti.StixDomainObject.add_external_reference = _noop
pycti.Report.add_stix_object_or_stix_relationship = _noop

return start_fn(self, job_id, runtime_configuration, task_id, *args, **kwargs)

return inner

return super()._monkeypatch(patches=[_configure])
20 changes: 0 additions & 20 deletions api_app/connectors_manager/connectors/slack.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from typing import Dict
from unittest.mock import patch

import slack_sdk
from slack_sdk.errors import SlackApiError

from api_app.connectors_manager.classes import Connector
from tests.mock_utils import if_mock_connections


class Slack(Connector):
Expand Down Expand Up @@ -36,21 +34,3 @@ def body(self) -> str:
def run(self) -> dict:
self.client.chat_postMessage(text=f"{self.title}\n{self.body}", channel=self._channel, mrkdwn=True)
return {}

@classmethod
def _monkeypatch(cls):
# flake8: noqa
class MockClient:
def __init__(self, *args, **kwargs): ...

def chat_postMessage(self, *args, **kwargs): ...

patches = [
if_mock_connections(
patch(
"slack_sdk.WebClient",
side_effect=MockClient,
)
)
]
return super()._monkeypatch(patches=patches)
26 changes: 20 additions & 6 deletions tests/api_app/connectors_manager/test_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -132,6 +137,15 @@ def handler(signum, frame):
signal.alarm(timeout_seconds)
try:
sub.start(job.pk, {}, uuid())
except TypeError as e:
Comment thread
PranavShukla7 marked this conversation as resolved.
# 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
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TypeError handling relies on matching a specific substring in the exception message. Since the message is not a stable contract, small wording changes in read_configured_params() could make this test start failing or start masking unexpected TypeErrors. Consider checking a more structured signal (e.g., a dedicated exception type for missing required params, or at least validating multiple expected fields like the "Required param" prefix + plugin module) so only the intended configuration-validation error is skipped.

Copilot uses AI. Check for mistakes.
except Exception as e:
self.fail(f"Connector {subclass.__name__} with config {config.name} failed {e}")
finally:
Expand Down
120 changes: 120 additions & 0 deletions tests/api_app/connectors_manager/unit_tests/base_test_class.py
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.
Comment thread
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()
30 changes: 30 additions & 0 deletions tests/api_app/connectors_manager/unit_tests/test_email_sender.py
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
Comment thread
PranavShukla7 marked this conversation as resolved.

Comment on lines +8 to +10
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests rely on Django's manage.py test (unittest discovery). Without an __init__.py in tests/api_app/connectors_manager/unit_tests/, that directory may not be treated as a package and the test modules under it can be skipped entirely during discovery. Add an __init__.py to the unit_tests directory so this test is actually executed in CI.

Copilot uses AI. Check for mistakes.

class EmailSenderTestCase(BaseConnectorTest):
connector_class = EmailSender

def get_mocked_response(self):
return patch("django.core.mail.EmailMessage.send", return_value="Email sent")

Comment thread
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"])
38 changes: 38 additions & 0 deletions tests/api_app/connectors_manager/unit_tests/test_slack.py
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
Comment thread
PranavShukla7 marked this conversation as resolved.

Comment on lines +8 to +10
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests rely on Django's manage.py test (unittest discovery). Without an __init__.py in tests/api_app/connectors_manager/unit_tests/, that directory may not be treated as a package and the test modules under it can be skipped entirely during discovery. Add an __init__.py to the unit_tests directory (consistent with the analyzers' unit_tests/* structure) so this test is actually executed in CI.

Copilot uses AI. Check for mistakes.

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, {})
Loading