Skip to content
Open
12 changes: 0 additions & 12 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 @@ -45,14 +44,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)
34 changes: 0 additions & 34 deletions api_app/connectors_manager/connectors/opencti.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,37 +227,3 @@ def run(self):
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])
19 changes: 0 additions & 19 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 @@ -37,20 +35,3 @@ def run(self) -> dict:
self.client.chat_postMessage(text=f"{self.title}\n{self.body}", channel=self._channel, mrkdwn=True)
return {}

Comment thread
PranavShukla7 marked this conversation as resolved.
Outdated
@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)
25 changes: 19 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,17 @@ 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 +136,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
121 changes: 121 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,121 @@
# 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.)
"""
config = ConnectorConfig.objects.get(
python_module__module__endswith=f".{connector_class_name}"
)

# Create required PluginConfigs if params are provided
if params:
for name, value in params.items():
param = Parameter.objects.get(
python_module=config.python_module, name=name
)
PluginConfig.objects.get_or_create(
parameter=param,
connector_config=config,
defaults={"value": value, "for_organization": False, "owner": None},
)
Comment thread
PranavShukla7 marked this conversation as resolved.
Outdated

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

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.
"""
if not self.connector_class:
self.skipTest("connector_class not set")

connector, job, config = self._setup_connector(
connector_class_name, observable_name, observable_type, params
)

patches = self.get_mocked_response()
with self._apply_patches(patches):
try:
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
except Exception as e:
self.fail(f"Connector {self.connector_class.__name__} failed: {e}")
Comment thread
PranavShukla7 marked this conversation as resolved.
Outdated
finally:
job.delete()
analyzable = Analyzable.objects.filter(name=observable_name).first()
if analyzable:
analyzable.delete()
Comment thread
PranavShukla7 marked this conversation as resolved.
Outdated
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