55import os
66from pathlib import Path
77from tempfile import TemporaryDirectory
8- from typing import Type
8+ from typing import TYPE_CHECKING
99
1010import pytest
1111from pydantic import BaseModel , ConfigDict , Field
1212from pydantic .v1 .utils import deep_update
1313
1414import config
1515from analysis .plugin import AnalysisPluginV0
16+ from test .common_helper import TEST_FW , TEST_TEXT_FILE , CommonDatabaseMock
1617from 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