Files
aprsd/tests/plugins/test_fortune.py
hemna 74288f6316 fix: handle missing fortune binary without raising (#292)
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.
2026-09-18 14:14:54 -04:00

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)