mirror of
https://github.com/craigerl/aprsd.git
synced 2026-10-12 02:04:18 -04:00
FileNotFoundError (OSError) from subprocess.check_output escaped the except subprocess.CalledProcessError handler, so a fortune binary that was removed between setup() and process() propagated an uncaught exception. TimeoutExpired (hung binary) had the same symptom. Catch (TimeoutExpired, OSError) and answer with a graceful "Fortune command failed" reply instead. Addresses review finding in docs/code-review-2026-09-18.md.
83 lines
3.0 KiB
Python
83 lines
3.0 KiB
Python
import subprocess
|
|
from unittest import mock
|
|
|
|
from oslo_config import cfg
|
|
|
|
from aprsd import conf # noqa: F401
|
|
from aprsd.plugins import fortune as fortune_plugin
|
|
|
|
from .. import fake, test_plugin
|
|
|
|
CONF = cfg.CONF
|
|
|
|
|
|
class TestFortunePlugin(test_plugin.TestPlugin):
|
|
@mock.patch('shutil.which')
|
|
def test_fortune_fail(self, mock_which):
|
|
mock_which.return_value = None
|
|
fortune = fortune_plugin.FortunePlugin()
|
|
expected = "FortunePlugin isn't enabled"
|
|
packet = fake.fake_packet(message='fortune')
|
|
actual = fortune.filter(packet)
|
|
self.assertEqual(expected, actual)
|
|
|
|
@mock.patch('subprocess.check_output')
|
|
@mock.patch('shutil.which')
|
|
def test_fortune_success(self, mock_which, mock_output):
|
|
mock_which.return_value = '/usr/bin/games/fortune'
|
|
mock_output.return_value = 'Funny fortune'
|
|
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
|
fortune = fortune_plugin.FortunePlugin()
|
|
|
|
expected = 'Funny fortune'
|
|
packet = fake.fake_packet(message='fortune')
|
|
actual = fortune.filter(packet)
|
|
self.assertEqual(expected, actual)
|
|
|
|
@mock.patch('subprocess.check_output')
|
|
@mock.patch('shutil.which')
|
|
def test_fortune_command_is_list_args_without_shell(self, mock_which, mock_output):
|
|
mock_which.return_value = '/usr/bin/games/fortune'
|
|
mock_output.return_value = 'Funny fortune'
|
|
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
|
fortune = fortune_plugin.FortunePlugin()
|
|
packet = fake.fake_packet(message='fortune')
|
|
fortune.filter(packet)
|
|
|
|
args, kwargs = mock_output.call_args
|
|
self.assertEqual(
|
|
args[0],
|
|
['/usr/bin/games/fortune', '-s', '-n', '60'],
|
|
)
|
|
self.assertNotIn('shell', kwargs)
|
|
|
|
@mock.patch(
|
|
'subprocess.check_output',
|
|
side_effect=FileNotFoundError(2, 'No such file or directory'),
|
|
)
|
|
@mock.patch('shutil.which')
|
|
def test_fortune_missing_binary_returns_graceful_reply(
|
|
self, mock_which, mock_output
|
|
):
|
|
"""A removed fortune binary answers gracefully instead of raising."""
|
|
mock_which.return_value = '/usr/bin/games/fortune'
|
|
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
|
fortune = fortune_plugin.FortunePlugin()
|
|
packet = fake.fake_packet(message='fortune')
|
|
actual = fortune.filter(packet)
|
|
self.assertIn('No such file or directory', actual)
|
|
|
|
@mock.patch(
|
|
'subprocess.check_output',
|
|
side_effect=subprocess.TimeoutExpired('fortune', 3),
|
|
)
|
|
@mock.patch('shutil.which')
|
|
def test_fortune_timeout_returns_graceful_reply(self, mock_which, mock_output):
|
|
"""A hung fortune command answers gracefully instead of raising."""
|
|
mock_which.return_value = '/usr/bin/games/fortune'
|
|
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
|
fortune = fortune_plugin.FortunePlugin()
|
|
packet = fake.fake_packet(message='fortune')
|
|
actual = fortune.filter(packet)
|
|
self.assertIn('failed', actual)
|