Files
meshcore-bot/tests/test_aurora_command.py
agessaman 53112c5f05 Refactor: streamline imports and enhance documentation in utility and service files
- Updated the `resolve_path` function in `utils.py` to clarify behavior regarding absolute paths.
- Changed type hints in `discord_bridge_service.py` for better clarity and consistency.
- Removed unused imports and unnecessary comments in various test files to improve code cleanliness and readability.
2026-03-18 18:27:05 -07:00

402 lines
15 KiB
Python

"""Tests for modules.commands.aurora_command — pure logic functions."""
import configparser
from unittest.mock import MagicMock, Mock, patch
import pytest
from modules.commands.aurora_command import AuroraCommand
from tests.conftest import mock_message
def _make_bot(with_location=False):
bot = MagicMock()
bot.logger = Mock()
config = configparser.ConfigParser()
config.add_section("Bot")
config.set("Bot", "bot_name", "TestBot")
if with_location:
config.set("Bot", "bot_latitude", "47.6")
config.set("Bot", "bot_longitude", "-122.3")
config.add_section("Channels")
config.set("Channels", "monitor_channels", "general")
config.set("Channels", "respond_to_dms", "true")
config.add_section("Keywords")
config.add_section("Weather")
config.set("Weather", "default_state", "WA")
config.set("Weather", "default_country", "US")
config.add_section("Aurora_Command")
config.set("Aurora_Command", "enabled", "true")
bot.config = config
bot.translator = MagicMock()
bot.translator.translate = Mock(side_effect=lambda key, **kw: key)
bot.command_manager = MagicMock()
bot.command_manager.monitor_channels = ["general"]
return bot
class TestProbIndicator:
"""Tests for _prob_indicator."""
def setup_method(self):
self.cmd = AuroraCommand(_make_bot())
def test_zero_returns_lowest_bar(self):
result = self.cmd._prob_indicator(0)
assert result == ""
def test_100_returns_highest_bar(self):
result = self.cmd._prob_indicator(100)
assert result == ""
def test_50_returns_mid_bar(self):
result = self.cmd._prob_indicator(50)
assert result in "▁▂▃▄▅▆▇█"
def test_all_values_return_valid_bar(self):
for pct in range(0, 101, 10):
result = self.cmd._prob_indicator(pct)
assert result in "▁▂▃▄▅▆▇█"
class TestFormatKpTime:
"""Tests for _format_kp_time."""
def setup_method(self):
self.cmd = AuroraCommand(_make_bot())
def test_empty_string_returns_dash(self):
assert self.cmd._format_kp_time("") == ""
def test_none_like_returns_dash(self):
assert self.cmd._format_kp_time(" ") == ""
def test_space_separated_format(self):
result = self.cmd._format_kp_time("2026-01-21 05:13:00")
assert isinstance(result, str)
assert len(result) > 0
def test_iso_format(self):
result = self.cmd._format_kp_time("2026-01-21T05:13:00")
assert isinstance(result, str)
assert len(result) > 0
def test_invalid_returns_dash(self):
result = self.cmd._format_kp_time("not-a-date")
assert result == ""
class TestGetBotLocation:
"""Tests for _get_bot_location."""
def test_returns_location_when_configured(self):
bot = _make_bot(with_location=True)
cmd = AuroraCommand(bot)
result = cmd._get_bot_location()
assert result is not None
lat, lon = result
assert abs(lat - 47.6) < 0.01
assert abs(lon - (-122.3)) < 0.01
def test_returns_none_when_not_configured(self):
bot = _make_bot(with_location=False)
cmd = AuroraCommand(bot)
result = cmd._get_bot_location()
assert result is None
class TestResolveLocation:
"""Tests for _resolve_location (pure location string parsing)."""
def setup_method(self):
self.cmd = AuroraCommand(_make_bot())
def test_coord_string_parsed(self):
msg = mock_message(content="aurora 47.6,-122.3")
lat, lon, label, err = self.cmd._resolve_location(msg, "47.6,-122.3")
assert lat == pytest.approx(47.6)
assert lon == pytest.approx(-122.3)
assert err is None
def test_invalid_lat_returns_error(self):
msg = mock_message(content="aurora 200,0")
lat, lon, label, err = self.cmd._resolve_location(msg, "200,0")
assert err is not None
def test_invalid_lon_returns_error(self):
msg = mock_message(content="aurora 0,200")
lat, lon, label, err = self.cmd._resolve_location(msg, "0,200")
assert err is not None
def test_no_location_uses_bot_location(self):
bot = _make_bot(with_location=True)
cmd = AuroraCommand(bot)
msg = mock_message(content="aurora")
# No companion location: db_manager returns nothing
bot.db_manager.execute_query.return_value = []
lat, lon, label, err = cmd._resolve_location(msg, None)
assert lat is not None
assert err is None
def test_no_location_no_bot_returns_error(self):
bot = _make_bot(with_location=False)
cmd = AuroraCommand(bot)
msg = mock_message(content="aurora")
# No companion, no bot location
bot.db_manager.execute_query.return_value = []
lat, lon, label, err = cmd._resolve_location(msg, None)
assert err is not None
class TestAuroraCanExecute:
"""Tests for can_execute."""
def test_enabled(self):
bot = _make_bot()
cmd = AuroraCommand(bot)
msg = mock_message(content="aurora", channel="general")
assert cmd.can_execute(msg) is True
def test_disabled(self):
bot = _make_bot()
bot.config.set("Aurora_Command", "enabled", "false")
cmd = AuroraCommand(bot)
msg = mock_message(content="aurora", channel="general")
assert cmd.can_execute(msg) is False
class TestResolveLocationExtended:
"""Extended tests for _resolve_location."""
def test_companion_location_used(self):
bot = _make_bot()
cmd = AuroraCommand(bot)
bot.db_manager.execute_query.return_value = [{"latitude": 47.5, "longitude": -122.1}]
msg = mock_message(content="aurora", sender_pubkey="a" * 64)
lat, lon, label, err = cmd._resolve_location(msg, None)
assert lat == pytest.approx(47.5)
assert lon == pytest.approx(-122.1)
assert err is None
def test_default_lat_lon_from_config(self):
bot = _make_bot()
bot.config.set("Aurora_Command", "default_lat", "48.0")
bot.config.set("Aurora_Command", "default_lon", "-122.5")
cmd = AuroraCommand(bot)
bot.db_manager.execute_query.return_value = []
msg = mock_message(content="aurora")
lat, lon, label, err = cmd._resolve_location(msg, None)
assert lat == pytest.approx(48.0)
assert err is None
def test_coords_value_error_returns_error(self):
bot = _make_bot()
cmd = AuroraCommand(bot)
msg = mock_message(content="aurora 47.6,-not-a-number")
lat, lon, label, err = cmd._resolve_location(msg, "47.6,-not-a-number")
# Non-matching regex so falls through to city path or ValueError
assert isinstance(err, (str, type(None)))
class TestAuroraExecute:
"""Tests for execute()."""
def test_no_location_no_bot_returns_error(self):
import asyncio
from unittest.mock import AsyncMock
bot = _make_bot(with_location=False)
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
bot.db_manager.execute_query.return_value = []
msg = mock_message(content="aurora", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
cmd.send_response.assert_called_once()
def test_execute_with_bot_location_success(self):
import asyncio
from unittest.mock import AsyncMock, MagicMock
bot = _make_bot(with_location=True)
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
bot.db_manager.execute_query.return_value = []
# Mock the aurora client data
mock_data = MagicMock()
mock_data.kp_index = 2.5
mock_data.kp_timestamp = "2026-01-21 05:13:00"
mock_data.aurora_probability = 15.0
with patch("modules.commands.aurora_command.NOAAAuroraClient") as MockClient:
MockClient.return_value.get_aurora_data.return_value = mock_data
msg = mock_message(content="aurora", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
cmd.send_response.assert_called_once()
def test_execute_kp_g3_severe(self):
import asyncio
from unittest.mock import AsyncMock, MagicMock
bot = _make_bot(with_location=True)
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
bot.db_manager.execute_query.return_value = []
mock_data = MagicMock()
mock_data.kp_index = 8.0 # >= 7
mock_data.kp_timestamp = ""
mock_data.aurora_probability = 95.0
with patch("modules.commands.aurora_command.NOAAAuroraClient") as MockClient:
MockClient.return_value.get_aurora_data.return_value = mock_data
msg = mock_message(content="aurora", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
def test_execute_kp_g1_g2(self):
import asyncio
from unittest.mock import AsyncMock, MagicMock
bot = _make_bot(with_location=True)
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
bot.db_manager.execute_query.return_value = []
mock_data = MagicMock()
mock_data.kp_index = 5.5 # >= 5 but < 7
mock_data.kp_timestamp = ""
mock_data.aurora_probability = 60.0
with patch("modules.commands.aurora_command.NOAAAuroraClient") as MockClient:
MockClient.return_value.get_aurora_data.return_value = mock_data
msg = mock_message(content="aurora", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
def test_execute_kp_unsettled(self):
import asyncio
from unittest.mock import AsyncMock, MagicMock
bot = _make_bot(with_location=True)
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
bot.db_manager.execute_query.return_value = []
mock_data = MagicMock()
mock_data.kp_index = 4.2 # >= 4 but < 5
mock_data.kp_timestamp = "2026-01-21T05:13:00"
mock_data.aurora_probability = 40.0
with patch("modules.commands.aurora_command.NOAAAuroraClient") as MockClient:
MockClient.return_value.get_aurora_data.return_value = mock_data
msg = mock_message(content="aurora", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
def test_execute_aurora_fetch_exception(self):
import asyncio
from unittest.mock import AsyncMock
bot = _make_bot(with_location=True)
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
bot.db_manager.execute_query.return_value = []
with patch("modules.commands.aurora_command.NOAAAuroraClient") as MockClient:
MockClient.return_value.get_aurora_data.side_effect = Exception("Network error")
msg = mock_message(content="aurora", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
cmd.send_response.assert_called_once()
def test_execute_with_coords_arg(self):
import asyncio
from unittest.mock import AsyncMock, MagicMock
bot = _make_bot()
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
mock_data = MagicMock()
mock_data.kp_index = 1.0
mock_data.kp_timestamp = ""
mock_data.aurora_probability = 5.0
with patch("modules.commands.aurora_command.NOAAAuroraClient") as MockClient:
MockClient.return_value.get_aurora_data.return_value = mock_data
msg = mock_message(content="aurora 47.6,-122.3", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
def test_execute_no_location_error_key(self):
import asyncio
from unittest.mock import AsyncMock
bot = _make_bot(with_location=False)
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
bot.db_manager.execute_query.return_value = []
# Simulate an error from _resolve_location
with patch.object(cmd, "_resolve_location", return_value=(None, None, None, "commands.aurora.no_location")):
msg = mock_message(content="aurora", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
def test_execute_error_key_zipcode(self):
import asyncio
from unittest.mock import AsyncMock
bot = _make_bot()
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
with patch.object(cmd, "_resolve_location", return_value=(None, None, None, "commands.aurora.no_location_zipcode")):
msg = mock_message(content="aurora 99999", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
def test_execute_error_key_city(self):
import asyncio
from unittest.mock import AsyncMock
bot = _make_bot()
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
with patch.object(cmd, "_resolve_location", return_value=(None, None, None, "commands.aurora.no_location_city")):
msg = mock_message(content="aurora Seattle", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
def test_execute_error_key_other(self):
import asyncio
from unittest.mock import AsyncMock
bot = _make_bot()
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
with patch.object(cmd, "_resolve_location", return_value=(None, None, None, "commands.aurora.error")):
msg = mock_message(content="aurora bad", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
def test_execute_response_truncated_at_max_length(self):
import asyncio
from unittest.mock import AsyncMock, MagicMock
bot = _make_bot(with_location=True)
cmd = AuroraCommand(bot)
cmd.send_response = AsyncMock(return_value=True)
bot.db_manager.execute_query.return_value = []
mock_data = MagicMock()
mock_data.kp_index = 0.5
mock_data.kp_timestamp = ""
mock_data.aurora_probability = 1.0
# Make translate return a very long string
cmd.translate = Mock(side_effect=lambda key, **kw: "x" * 300 if key == "commands.aurora.response" else key)
cmd.get_max_message_length = Mock(return_value=100)
with patch("modules.commands.aurora_command.NOAAAuroraClient") as MockClient:
MockClient.return_value.get_aurora_data.return_value = mock_data
msg = mock_message(content="aurora", channel="general")
result = asyncio.run(cmd.execute(msg))
assert result is True
# Response should be truncated
call_text = cmd.send_response.call_args[0][1]
assert len(call_text) <= 103 # 100 + "..."