Skip to content

Commit 6e81313

Browse files
committed
refactor: moved unit.conftest to src/conftest
so that plugin tests can access the unit test fixtures
1 parent d257443 commit 6e81313

8 files changed

Lines changed: 188 additions & 212 deletions

File tree

docsrc/modules/test.rst

Lines changed: 0 additions & 8 deletions
This file was deleted.

docsrc/modules/test.unit.conftest.rst

Lines changed: 0 additions & 6 deletions
This file was deleted.

docsrc/modules/test.unit.rst

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/conftest.py

Lines changed: 185 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@
55
import os
66
from pathlib import Path
77
from tempfile import TemporaryDirectory
8-
from typing import Type
8+
from typing import TYPE_CHECKING
99

1010
import pytest
1111
from pydantic import BaseModel, ConfigDict, Field
1212
from pydantic.v1.utils import deep_update
1313

1414
import config
1515
from analysis.plugin import AnalysisPluginV0
16+
from test.common_helper import TEST_FW, TEST_TEXT_FILE, CommonDatabaseMock
1617
from test.conftest import merge_markers
18+
from web_interface.frontend_main import WebFrontEnd
19+
from web_interface.security.authentication import add_flask_security_to_app
20+
21+
if TYPE_CHECKING:
22+
from collections.abc import Iterator
1723

1824

1925
@pytest.fixture
20-
def docker_mount_base_dir() -> str:
26+
def docker_mount_base_dir() -> Iterator[str]:
2127
docker_gid = grp.getgrnam('docker').gr_gid
2228

2329
with TemporaryDirectory(prefix='fact-docker-mount-base-dir') as tmp_dir:
@@ -27,7 +33,7 @@ def docker_mount_base_dir() -> str:
2733

2834

2935
@pytest.fixture
30-
def firmware_file_storage_directory() -> str:
36+
def firmware_file_storage_directory() -> Iterator[str]:
3137
with TemporaryDirectory(prefix='fact-firmware-file-storage-directory') as tmp_dir:
3238
yield tmp_dir
3339

@@ -184,7 +190,7 @@ class AnalysisPluginTestConfig(BaseModel):
184190
model_config = ConfigDict(arbitrary_types_allowed=True)
185191

186192
#: The class of the plugin to be tested. It will most probably be called ``AnalysisPlugin``.
187-
plugin_class: Type[AnalysisPluginV0] = AnalysisPluginV0
193+
plugin_class: type[AnalysisPluginV0] = AnalysisPluginV0
188194
#: Whether or not to start the workers (see ``AnalysisPlugin.start``).
189195
#: Not supported for AnalysisPluginV0
190196
start_processes: bool = False
@@ -238,11 +244,180 @@ def my_fancy_test(analysis_plugin, monkeypatch):
238244
# FIXME now with AnalysisPluginV0 analysis plugins became way simpler
239245
# We might want to delete everything from AnalysisPluginTestConfig in the future
240246
PluginClass = test_config.plugin_class # noqa: N806
241-
assert (
242-
test_config.init_kwargs == {}
243-
), 'AnalysisPluginTestConfig.init_kwargs must be empty for AnalysisPluginV0 instances'
244-
assert (
245-
not test_config.start_processes
246-
), 'AnalysisPluginTestConfig.start_processes cannot be True for AnalysisPluginV0 instances'
247+
assert test_config.init_kwargs == {}, (
248+
'AnalysisPluginTestConfig.init_kwargs must be empty for AnalysisPluginV0 instances'
249+
)
250+
assert not test_config.start_processes, (
251+
'AnalysisPluginTestConfig.start_processes cannot be True for AnalysisPluginV0 instances'
252+
)
247253

248254
return PluginClass()
255+
256+
257+
# === Unit test classes and fixtures ===
258+
259+
260+
class CommonIntercomMock:
261+
task_list = None
262+
_common_fields = ('0.1.0', [], [], [], 1)
263+
264+
def __init__(self, *_, **__):
265+
pass
266+
267+
def get_available_analysis_plugins(self):
268+
return {
269+
'default_plugin': ('default plugin description', False, {'default': True}, *self._common_fields),
270+
'mandatory_plugin': ('mandatory plugin description', True, {'default': False}, *self._common_fields),
271+
'optional_plugin': ('optional plugin description', False, {'default': False}, *self._common_fields),
272+
'file_type': ('file_type plugin', False, {'default': False}, *self._common_fields),
273+
'unpacker': ('Additional information provided by the unpacker', True, False),
274+
}
275+
276+
def shutdown(self):
277+
pass
278+
279+
@staticmethod
280+
def peek_in_binary(*_):
281+
return b'foobar'
282+
283+
@staticmethod
284+
def get_binary_and_filename(uid):
285+
if uid == TEST_FW.uid:
286+
return TEST_FW.binary, TEST_FW.file_name
287+
if uid == TEST_TEXT_FILE.uid:
288+
return TEST_TEXT_FILE.binary, TEST_TEXT_FILE.file_name
289+
return None
290+
291+
@staticmethod
292+
def get_repacked_binary_and_file_name(uid):
293+
if uid == TEST_FW.uid:
294+
return TEST_FW.binary, f'{TEST_FW.file_name}.tar.gz'
295+
return None, None
296+
297+
@staticmethod
298+
def add_binary_search_request(*_):
299+
return 'binary_search_id'
300+
301+
@staticmethod
302+
def get_binary_search_result(uid):
303+
if uid == 'binary_search_id':
304+
return {'test_rule': ['test_uid']}, b'some yara rule'
305+
return None, None
306+
307+
def add_compare_task(self, compare_id, force=False):
308+
self.task_list.append((compare_id, force))
309+
310+
def add_analysis_task(self, task):
311+
self.task_list.append(task)
312+
313+
def add_re_analyze_task(self, task, unpack=True):
314+
self.task_list.append(task)
315+
316+
def cancel_analysis(self, root_uid):
317+
self.task_list.append(root_uid)
318+
319+
def get_yara_error(self, rule):
320+
if isinstance(rule, bytes):
321+
rule = rule.decode(errors='ignore')
322+
if 'invalid' in rule:
323+
return 'SyntaxError: line 1: syntax error, unexpected identifier'
324+
return ''
325+
326+
327+
class FrontendDatabaseMock:
328+
"""A class mocking :py:class:`~web_interface.frontend_database.FrontendDatabase`."""
329+
330+
def __init__(self, db_mock: CommonDatabaseMock):
331+
"""
332+
The Constructor.
333+
334+
:param db_mock: An object providing every function needed for a test.
335+
"""
336+
self.frontend = db_mock
337+
self.editing = db_mock
338+
self.admin = db_mock
339+
self.comparison = db_mock
340+
self.template = db_mock
341+
self.stats_viewer = db_mock
342+
self.stats_updater = db_mock
343+
344+
345+
class _UserDbMock:
346+
class session: # noqa: N801
347+
@staticmethod
348+
def commit():
349+
pass
350+
351+
@staticmethod
352+
def rollback():
353+
pass
354+
355+
356+
class StatusInterfaceMock:
357+
def __init__(self):
358+
self._status = {'current_analyses': {}, 'recently_finished_analyses': {}}
359+
360+
def set_analysis_status(self, status: dict):
361+
self._status = status
362+
363+
def get_analysis_status(self):
364+
return self._status
365+
366+
367+
class WebInterfaceUnitTestConfig(BaseModel):
368+
"""A class configuring the :py:func:`web_frontend` fixture."""
369+
370+
#: A class that can be instanced to mock every ``@property`` of
371+
#: :py:class:`~web_interface.frontend_database.FrontendDatabase`.
372+
#: See also: The documentation of :py:class:`FrontendDatabaseMock`
373+
database_mock_class: type = CommonDatabaseMock
374+
#: A class mocking :py:class:`~intercom.front_end_binding.InterComFrontEndBinding`
375+
intercom_mock_class: type[CommonIntercomMock] = CommonIntercomMock
376+
#: A class mocking :py:class:`~storage.redis_status_interface.RedisStatusInterface`
377+
status_mock_class: type[StatusInterfaceMock] = StatusInterfaceMock
378+
379+
380+
@pytest.fixture
381+
def intercom_task_list() -> list:
382+
"""A fixture used to add tasks in the :py:class:`CommonIntercomMock`.
383+
It can be used to inspect what tasks where added"""
384+
return []
385+
386+
387+
@pytest.fixture
388+
def web_frontend(request, monkeypatch, intercom_task_list) -> WebFrontEnd:
389+
"""Returns an instance of :py:class:`~web_interface.frontend_main.WebFrontEnd`.
390+
This fixture can be configured by providing an instance of :py:class:`WebInterfaceUnitTestConfig` as a marker
391+
called ``WebInterfaceUnitTestConfig``.
392+
393+
.. seealso::
394+
395+
The fixture :py:func:`intercom_task_list`.
396+
"""
397+
test_config = merge_markers(request, 'WebInterfaceUnitTestConfig', WebInterfaceUnitTestConfig)
398+
399+
db_mock_instance = test_config.database_mock_class()
400+
IntercomMockClass = test_config.intercom_mock_class # noqa: N806
401+
402+
def _add_flask_security_to_app_mock(app):
403+
add_flask_security_to_app(app)
404+
return _UserDbMock(), db_mock_instance
405+
406+
monkeypatch.setattr('web_interface.frontend_main.add_flask_security_to_app', _add_flask_security_to_app_mock)
407+
408+
monkeypatch.setattr(IntercomMockClass, 'task_list', intercom_task_list)
409+
# Note: The intercom argument is only the class. It gets instanced when intercom access in needed by `ConnectTo`.
410+
frontend = WebFrontEnd(
411+
db=FrontendDatabaseMock(db_mock_instance),
412+
intercom=IntercomMockClass,
413+
status_interface=test_config.status_mock_class(),
414+
)
415+
frontend.app.config['TESTING'] = True
416+
417+
return frontend
418+
419+
420+
@pytest.fixture
421+
def test_client(web_frontend):
422+
"""Shorthand for ``web_frontend.app.test_client``"""
423+
return web_frontend.app.test_client()

0 commit comments

Comments
 (0)