Initial commit with translated description
This commit is contained in:
390
tests/test_alerts_extended.py
Normal file
390
tests/test_alerts_extended.py
Normal file
@@ -0,0 +1,390 @@
|
||||
"""Extended tests for alerts.py - price target alerts."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
from io import StringIO
|
||||
|
||||
import pytest
|
||||
|
||||
# Add scripts to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from alerts import (
|
||||
load_alerts,
|
||||
save_alerts,
|
||||
get_alert_by_ticker,
|
||||
format_price,
|
||||
cmd_list,
|
||||
cmd_set,
|
||||
cmd_delete,
|
||||
cmd_snooze,
|
||||
cmd_update,
|
||||
SUPPORTED_CURRENCIES,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_alerts_data():
|
||||
"""Sample alerts data for testing."""
|
||||
return {
|
||||
"_meta": {"version": 1, "supported_currencies": SUPPORTED_CURRENCIES},
|
||||
"alerts": [
|
||||
{
|
||||
"ticker": "AAPL",
|
||||
"target_price": 150.0,
|
||||
"currency": "USD",
|
||||
"note": "Buy Apple",
|
||||
"set_by": "art",
|
||||
"set_date": "2026-01-15",
|
||||
"status": "active",
|
||||
"snooze_until": None,
|
||||
"triggered_count": 0,
|
||||
"last_triggered": None,
|
||||
},
|
||||
{
|
||||
"ticker": "TSLA",
|
||||
"target_price": 200.0,
|
||||
"currency": "USD",
|
||||
"note": "Buy Tesla",
|
||||
"set_by": "",
|
||||
"set_date": "2026-01-20",
|
||||
"status": "active",
|
||||
"snooze_until": None,
|
||||
"triggered_count": 5,
|
||||
"last_triggered": "2026-01-26T10:00:00",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def alerts_file(tmp_path, sample_alerts_data):
|
||||
"""Create a temporary alerts file."""
|
||||
alerts_path = tmp_path / "alerts.json"
|
||||
alerts_path.write_text(json.dumps(sample_alerts_data))
|
||||
return alerts_path
|
||||
|
||||
|
||||
class TestLoadAlerts:
|
||||
"""Tests for load_alerts()."""
|
||||
|
||||
def test_load_existing_file(self, alerts_file, monkeypatch):
|
||||
"""Load alerts from existing file."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
data = load_alerts()
|
||||
|
||||
assert "_meta" in data
|
||||
assert len(data["alerts"]) == 2
|
||||
assert data["alerts"][0]["ticker"] == "AAPL"
|
||||
|
||||
def test_load_missing_file(self, tmp_path, monkeypatch):
|
||||
"""Return default structure when file doesn't exist."""
|
||||
missing_path = tmp_path / "missing.json"
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", missing_path)
|
||||
|
||||
data = load_alerts()
|
||||
|
||||
assert data["_meta"]["version"] == 1
|
||||
assert data["alerts"] == []
|
||||
assert "supported_currencies" in data["_meta"]
|
||||
|
||||
|
||||
class TestSaveAlerts:
|
||||
"""Tests for save_alerts()."""
|
||||
|
||||
def test_save_updates_timestamp(self, tmp_path, sample_alerts_data, monkeypatch):
|
||||
"""Save should update the updated_at field."""
|
||||
alerts_path = tmp_path / "alerts.json"
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_path)
|
||||
|
||||
save_alerts(sample_alerts_data)
|
||||
|
||||
saved = json.loads(alerts_path.read_text())
|
||||
assert "updated_at" in saved["_meta"]
|
||||
|
||||
def test_save_preserves_data(self, tmp_path, sample_alerts_data, monkeypatch):
|
||||
"""Save should preserve all alert data."""
|
||||
alerts_path = tmp_path / "alerts.json"
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_path)
|
||||
|
||||
save_alerts(sample_alerts_data)
|
||||
|
||||
saved = json.loads(alerts_path.read_text())
|
||||
assert len(saved["alerts"]) == 2
|
||||
assert saved["alerts"][0]["ticker"] == "AAPL"
|
||||
|
||||
|
||||
class TestGetAlertByTicker:
|
||||
"""Tests for get_alert_by_ticker()."""
|
||||
|
||||
def test_find_existing_alert(self, sample_alerts_data):
|
||||
"""Find alert by ticker."""
|
||||
alerts = sample_alerts_data["alerts"]
|
||||
result = get_alert_by_ticker(alerts, "AAPL")
|
||||
|
||||
assert result is not None
|
||||
assert result["ticker"] == "AAPL"
|
||||
assert result["target_price"] == 150.0
|
||||
|
||||
def test_find_case_insensitive(self, sample_alerts_data):
|
||||
"""Find alert regardless of case."""
|
||||
alerts = sample_alerts_data["alerts"]
|
||||
result = get_alert_by_ticker(alerts, "aapl")
|
||||
|
||||
assert result is not None
|
||||
assert result["ticker"] == "AAPL"
|
||||
|
||||
def test_not_found_returns_none(self, sample_alerts_data):
|
||||
"""Return None for non-existent ticker."""
|
||||
alerts = sample_alerts_data["alerts"]
|
||||
result = get_alert_by_ticker(alerts, "GOOG")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestFormatPrice:
|
||||
"""Tests for format_price()."""
|
||||
|
||||
def test_format_usd(self):
|
||||
"""Format USD price."""
|
||||
assert format_price(150.50, "USD") == "$150.50"
|
||||
assert format_price(1234.56, "USD") == "$1,234.56"
|
||||
|
||||
def test_format_eur(self):
|
||||
"""Format EUR price."""
|
||||
assert format_price(100.00, "EUR") == "€100.00"
|
||||
|
||||
def test_format_jpy(self):
|
||||
"""Format JPY without decimals."""
|
||||
assert format_price(15000, "JPY") == "¥15,000"
|
||||
|
||||
def test_format_sgd(self):
|
||||
"""Format SGD price."""
|
||||
assert format_price(50.00, "SGD") == "S$50.00"
|
||||
|
||||
def test_format_mxn(self):
|
||||
"""Format MXN price."""
|
||||
assert format_price(100.00, "MXN") == "MX$100.00"
|
||||
|
||||
def test_format_unknown_currency(self):
|
||||
"""Format unknown currency with code prefix."""
|
||||
result = format_price(100.00, "GBP")
|
||||
assert "GBP" in result
|
||||
assert "100.00" in result
|
||||
|
||||
|
||||
class TestCmdList:
|
||||
"""Tests for cmd_list()."""
|
||||
|
||||
def test_list_empty_alerts(self, tmp_path, monkeypatch, capsys):
|
||||
"""List with no alerts."""
|
||||
alerts_path = tmp_path / "alerts.json"
|
||||
alerts_path.write_text(json.dumps({"_meta": {}, "alerts": []}))
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_path)
|
||||
|
||||
cmd_list(Namespace())
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "No price alerts set" in captured.out
|
||||
|
||||
def test_list_active_alerts(self, alerts_file, monkeypatch, capsys):
|
||||
"""List active alerts."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
cmd_list(Namespace())
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Price Alerts" in captured.out
|
||||
assert "AAPL" in captured.out
|
||||
assert "$150.00" in captured.out
|
||||
|
||||
def test_list_snoozed_alerts(self, tmp_path, monkeypatch, capsys):
|
||||
"""List snoozed alerts separately."""
|
||||
future = (datetime.now() + timedelta(days=7)).isoformat()
|
||||
data = {
|
||||
"_meta": {},
|
||||
"alerts": [
|
||||
{"ticker": "AAPL", "target_price": 150, "currency": "USD", "snooze_until": future}
|
||||
],
|
||||
}
|
||||
alerts_path = tmp_path / "alerts.json"
|
||||
alerts_path.write_text(json.dumps(data))
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_path)
|
||||
|
||||
cmd_list(Namespace())
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Snoozed" in captured.out
|
||||
assert "AAPL" in captured.out
|
||||
|
||||
|
||||
class TestCmdSet:
|
||||
"""Tests for cmd_set()."""
|
||||
|
||||
def test_set_new_alert(self, alerts_file, monkeypatch, capsys):
|
||||
"""Set a new alert."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
with patch("alerts.get_fetch_market_data") as mock_fmd:
|
||||
mock_fmd.return_value = Mock(return_value={"GOOG": {"price": 175.0}})
|
||||
|
||||
args = Namespace(ticker="GOOG", target=150.0, currency="USD", note="Buy Google", user="art")
|
||||
cmd_set(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Alert set: GOOG" in captured.out
|
||||
|
||||
data = json.loads(alerts_file.read_text())
|
||||
goog = next((a for a in data["alerts"] if a["ticker"] == "GOOG"), None)
|
||||
assert goog is not None
|
||||
assert goog["target_price"] == 150.0
|
||||
|
||||
def test_set_duplicate_alert(self, alerts_file, monkeypatch, capsys):
|
||||
"""Cannot set duplicate alert."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="AAPL", target=140.0, currency="USD", note="", user="")
|
||||
cmd_set(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "already exists" in captured.out
|
||||
|
||||
def test_set_invalid_target(self, alerts_file, monkeypatch, capsys):
|
||||
"""Reject invalid target price."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="GOOG", target=-10.0, currency="USD", note="", user="")
|
||||
cmd_set(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "must be greater than 0" in captured.out
|
||||
|
||||
def test_set_invalid_currency(self, alerts_file, monkeypatch, capsys):
|
||||
"""Reject invalid currency."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="GOOG", target=150.0, currency="XYZ", note="", user="")
|
||||
cmd_set(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "not supported" in captured.out
|
||||
|
||||
|
||||
class TestCmdDelete:
|
||||
"""Tests for cmd_delete()."""
|
||||
|
||||
def test_delete_existing_alert(self, alerts_file, monkeypatch, capsys):
|
||||
"""Delete an existing alert."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="AAPL")
|
||||
cmd_delete(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Alert deleted: AAPL" in captured.out
|
||||
|
||||
data = json.loads(alerts_file.read_text())
|
||||
assert not any(a["ticker"] == "AAPL" for a in data["alerts"])
|
||||
|
||||
def test_delete_nonexistent_alert(self, alerts_file, monkeypatch, capsys):
|
||||
"""Cannot delete non-existent alert."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="GOOG")
|
||||
cmd_delete(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "No alert found" in captured.out
|
||||
|
||||
|
||||
class TestCmdSnooze:
|
||||
"""Tests for cmd_snooze()."""
|
||||
|
||||
def test_snooze_alert(self, alerts_file, monkeypatch, capsys):
|
||||
"""Snooze an alert."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="AAPL", days=7)
|
||||
cmd_snooze(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Alert snoozed: AAPL" in captured.out
|
||||
|
||||
data = json.loads(alerts_file.read_text())
|
||||
aapl = next(a for a in data["alerts"] if a["ticker"] == "AAPL")
|
||||
assert aapl["snooze_until"] is not None
|
||||
|
||||
def test_snooze_nonexistent_alert(self, alerts_file, monkeypatch, capsys):
|
||||
"""Cannot snooze non-existent alert."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="GOOG", days=7)
|
||||
cmd_snooze(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "No alert found" in captured.out
|
||||
|
||||
def test_snooze_default_days(self, alerts_file, monkeypatch, capsys):
|
||||
"""Default snooze is 7 days."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="AAPL", days=None)
|
||||
cmd_snooze(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Alert snoozed" in captured.out
|
||||
|
||||
|
||||
class TestCmdUpdate:
|
||||
"""Tests for cmd_update()."""
|
||||
|
||||
def test_update_target_price(self, alerts_file, monkeypatch, capsys):
|
||||
"""Update alert target price."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="AAPL", target=140.0, note=None)
|
||||
cmd_update(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "Alert updated: AAPL" in captured.out
|
||||
assert "$150.00" in captured.out # Old price
|
||||
assert "$140.00" in captured.out # New price
|
||||
|
||||
data = json.loads(alerts_file.read_text())
|
||||
aapl = next(a for a in data["alerts"] if a["ticker"] == "AAPL")
|
||||
assert aapl["target_price"] == 140.0
|
||||
|
||||
def test_update_with_note(self, alerts_file, monkeypatch, capsys):
|
||||
"""Update alert with new note."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="AAPL", target=145.0, note="New buy zone")
|
||||
cmd_update(args)
|
||||
|
||||
data = json.loads(alerts_file.read_text())
|
||||
aapl = next(a for a in data["alerts"] if a["ticker"] == "AAPL")
|
||||
assert aapl["note"] == "New buy zone"
|
||||
|
||||
def test_update_nonexistent_alert(self, alerts_file, monkeypatch, capsys):
|
||||
"""Cannot update non-existent alert."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="GOOG", target=150.0, note=None)
|
||||
cmd_update(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "No alert found" in captured.out
|
||||
|
||||
def test_update_invalid_target(self, alerts_file, monkeypatch, capsys):
|
||||
"""Reject invalid target price on update."""
|
||||
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
|
||||
|
||||
args = Namespace(ticker="AAPL", target=-10.0, note=None)
|
||||
cmd_update(args)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "must be greater than 0" in captured.out
|
||||
Reference in New Issue
Block a user