|
|
""" |
|
|
Tests for validation utilities. |
|
|
""" |
|
|
|
|
|
import pandas as pd |
|
|
import pytest |
|
|
|
|
|
from src.folio.exceptions import DataError |
|
|
from src.folio.validation import ( |
|
|
clean_numeric_value, |
|
|
extract_option_data, |
|
|
validate_dataframe, |
|
|
validate_option_data, |
|
|
) |
|
|
|
|
|
|
|
|
class TestValidateOptionData: |
|
|
"""Tests for validate_option_data function.""" |
|
|
|
|
|
def test_valid_option_data(self): |
|
|
"""Test with valid option data.""" |
|
|
option_row = pd.Series( |
|
|
{ |
|
|
"Description": "SPY JUN 15 2025 $100 CALL", |
|
|
"Quantity": "2", |
|
|
"Last Price": "5.00", |
|
|
} |
|
|
) |
|
|
|
|
|
description, quantity, price = validate_option_data(option_row) |
|
|
|
|
|
assert description == "SPY JUN 15 2025 $100 CALL" |
|
|
assert quantity == 2 |
|
|
assert price == 5.0 |
|
|
|
|
|
def test_missing_description(self): |
|
|
"""Test with missing description.""" |
|
|
option_row = pd.Series( |
|
|
{ |
|
|
"Description": None, |
|
|
"Quantity": "2", |
|
|
"Last Price": "5.00", |
|
|
} |
|
|
) |
|
|
|
|
|
with pytest.raises(DataError, match="Missing description"): |
|
|
validate_option_data(option_row) |
|
|
|
|
|
def test_missing_quantity(self): |
|
|
"""Test with missing quantity.""" |
|
|
option_row = pd.Series( |
|
|
{ |
|
|
"Description": "SPY JUN 15 2025 $100 CALL", |
|
|
"Quantity": None, |
|
|
"Last Price": "5.00", |
|
|
} |
|
|
) |
|
|
|
|
|
with pytest.raises(DataError, match="Missing quantity"): |
|
|
validate_option_data(option_row) |
|
|
|
|
|
def test_invalid_quantity(self): |
|
|
"""Test with invalid quantity format.""" |
|
|
option_row = pd.Series( |
|
|
{ |
|
|
"Description": "SPY JUN 15 2025 $100 CALL", |
|
|
"Quantity": "not a number", |
|
|
"Last Price": "5.00", |
|
|
} |
|
|
) |
|
|
|
|
|
with pytest.raises(DataError, match="Invalid quantity format"): |
|
|
validate_option_data(option_row) |
|
|
|
|
|
def test_missing_price(self): |
|
|
"""Test with missing price.""" |
|
|
option_row = pd.Series( |
|
|
{ |
|
|
"Description": "SPY JUN 15 2025 $100 CALL", |
|
|
"Quantity": "2", |
|
|
"Last Price": None, |
|
|
} |
|
|
) |
|
|
|
|
|
with pytest.raises(DataError, match="Missing price"): |
|
|
validate_option_data(option_row) |
|
|
|
|
|
def test_invalid_price(self): |
|
|
"""Test with invalid price format.""" |
|
|
option_row = pd.Series( |
|
|
{ |
|
|
"Description": "SPY JUN 15 2025 $100 CALL", |
|
|
"Quantity": "2", |
|
|
"Last Price": "not a price", |
|
|
} |
|
|
) |
|
|
|
|
|
with pytest.raises(DataError, match="Invalid price format"): |
|
|
validate_option_data(option_row) |
|
|
|
|
|
def test_custom_field_names(self): |
|
|
"""Test with custom field names.""" |
|
|
option_row = pd.Series( |
|
|
{ |
|
|
"OptionDesc": "SPY JUN 15 2025 $100 CALL", |
|
|
"OptionQty": "2", |
|
|
"OptionPrice": "5.00", |
|
|
} |
|
|
) |
|
|
|
|
|
description, quantity, price = validate_option_data( |
|
|
option_row, |
|
|
description_field="OptionDesc", |
|
|
quantity_field="OptionQty", |
|
|
price_field="OptionPrice", |
|
|
) |
|
|
|
|
|
assert description == "SPY JUN 15 2025 $100 CALL" |
|
|
assert quantity == 2 |
|
|
assert price == 5.0 |
|
|
|
|
|
|
|
|
class TestExtractOptionData: |
|
|
"""Tests for extract_option_data function.""" |
|
|
|
|
|
def test_extract_valid_options(self): |
|
|
"""Test extracting valid options.""" |
|
|
option_df = pd.DataFrame( |
|
|
[ |
|
|
{ |
|
|
"Description": "SPY JUN 15 2025 $100 CALL", |
|
|
"Symbol": "-SPY", |
|
|
"Quantity": "2", |
|
|
"Last Price": "5.00", |
|
|
"Current Value": "1000.00", |
|
|
}, |
|
|
{ |
|
|
"Description": "SPY JUN 15 2025 $110 CALL", |
|
|
"Symbol": "-SPY", |
|
|
"Quantity": "-1", |
|
|
"Last Price": "2.00", |
|
|
"Current Value": "-200.00", |
|
|
}, |
|
|
] |
|
|
) |
|
|
|
|
|
options_data = extract_option_data(option_df) |
|
|
|
|
|
assert len(options_data) == 2 |
|
|
assert options_data[0]["description"] == "SPY JUN 15 2025 $100 CALL" |
|
|
assert options_data[0]["quantity"] == 2 |
|
|
assert options_data[0]["price"] == 5.0 |
|
|
assert options_data[0]["symbol"] == "-SPY" |
|
|
assert "row_index" in options_data[0] |
|
|
|
|
|
assert options_data[1]["description"] == "SPY JUN 15 2025 $110 CALL" |
|
|
assert options_data[1]["quantity"] == -1 |
|
|
assert options_data[1]["price"] == 2.0 |
|
|
|
|
|
def test_extract_with_filter(self): |
|
|
"""Test extracting options with a filter function.""" |
|
|
option_df = pd.DataFrame( |
|
|
[ |
|
|
{ |
|
|
"Description": "SPY JUN 15 2025 $100 CALL", |
|
|
"Symbol": "-SPY", |
|
|
"Quantity": "2", |
|
|
"Last Price": "5.00", |
|
|
}, |
|
|
{ |
|
|
"Description": "AAPL JUN 15 2025 $200 CALL", |
|
|
"Symbol": "-AAPL", |
|
|
"Quantity": "1", |
|
|
"Last Price": "10.00", |
|
|
}, |
|
|
] |
|
|
) |
|
|
|
|
|
|
|
|
options_data = extract_option_data( |
|
|
option_df, |
|
|
filter_func=lambda row: row["Symbol"] == "-SPY", |
|
|
) |
|
|
|
|
|
assert len(options_data) == 1 |
|
|
assert options_data[0]["description"] == "SPY JUN 15 2025 $100 CALL" |
|
|
assert options_data[0]["symbol"] == "-SPY" |
|
|
|
|
|
def test_extract_with_invalid_options(self): |
|
|
"""Test extracting options with some invalid data.""" |
|
|
option_df = pd.DataFrame( |
|
|
[ |
|
|
{ |
|
|
"Description": "SPY JUN 15 2025 $100 CALL", |
|
|
"Symbol": "-SPY", |
|
|
"Quantity": "2", |
|
|
"Last Price": "5.00", |
|
|
}, |
|
|
{ |
|
|
"Description": None, |
|
|
"Symbol": "-AAPL", |
|
|
"Quantity": "1", |
|
|
"Last Price": "10.00", |
|
|
}, |
|
|
{ |
|
|
"Description": "AAPL JUN 15 2025 $200 CALL", |
|
|
"Symbol": "-AAPL", |
|
|
"Quantity": "not a number", |
|
|
"Last Price": "10.00", |
|
|
}, |
|
|
] |
|
|
) |
|
|
|
|
|
options_data = extract_option_data(option_df) |
|
|
|
|
|
|
|
|
assert len(options_data) == 1 |
|
|
assert options_data[0]["description"] == "SPY JUN 15 2025 $100 CALL" |
|
|
|
|
|
def test_extract_without_row_index(self): |
|
|
"""Test extracting options without including row index.""" |
|
|
option_df = pd.DataFrame( |
|
|
[ |
|
|
{ |
|
|
"Description": "SPY JUN 15 2025 $100 CALL", |
|
|
"Symbol": "-SPY", |
|
|
"Quantity": "2", |
|
|
"Last Price": "5.00", |
|
|
}, |
|
|
] |
|
|
) |
|
|
|
|
|
options_data = extract_option_data(option_df, include_row_index=False) |
|
|
|
|
|
assert len(options_data) == 1 |
|
|
assert "row_index" not in options_data[0] |
|
|
|
|
|
|
|
|
class TestValidateDataframe: |
|
|
"""Tests for validate_dataframe function.""" |
|
|
|
|
|
def test_valid_dataframe(self): |
|
|
"""Test with a valid DataFrame.""" |
|
|
df = pd.DataFrame( |
|
|
{ |
|
|
"Symbol": ["SPY", "AAPL"], |
|
|
"Quantity": [10, 20], |
|
|
"Price": [100.0, 200.0], |
|
|
} |
|
|
) |
|
|
|
|
|
result = validate_dataframe(df, ["Symbol", "Quantity", "Price"]) |
|
|
|
|
|
|
|
|
assert result is df |
|
|
|
|
|
def test_none_dataframe(self): |
|
|
"""Test with None DataFrame.""" |
|
|
with pytest.raises(DataError, match="dataframe is None"): |
|
|
validate_dataframe(None, ["Symbol"]) |
|
|
|
|
|
def test_empty_dataframe(self): |
|
|
"""Test with empty DataFrame.""" |
|
|
df = pd.DataFrame() |
|
|
|
|
|
with pytest.raises(DataError, match="dataframe is empty"): |
|
|
validate_dataframe(df, ["Symbol"]) |
|
|
|
|
|
def test_missing_columns(self): |
|
|
"""Test with missing required columns.""" |
|
|
df = pd.DataFrame( |
|
|
{ |
|
|
"Symbol": ["SPY", "AAPL"], |
|
|
"Quantity": [10, 20], |
|
|
} |
|
|
) |
|
|
|
|
|
with pytest.raises(DataError, match="missing required columns: Price"): |
|
|
validate_dataframe(df, ["Symbol", "Quantity", "Price"]) |
|
|
|
|
|
def test_custom_name(self): |
|
|
"""Test with custom DataFrame name.""" |
|
|
with pytest.raises(DataError, match="portfolio is None"): |
|
|
validate_dataframe(None, ["Symbol"], name="portfolio") |
|
|
|
|
|
|
|
|
class TestCleanNumericValue: |
|
|
"""Tests for clean_numeric_value function.""" |
|
|
|
|
|
def test_valid_numeric(self): |
|
|
"""Test with valid numeric values.""" |
|
|
assert clean_numeric_value(10) == 10.0 |
|
|
assert clean_numeric_value(10.5) == 10.5 |
|
|
assert clean_numeric_value("10") == 10.0 |
|
|
assert clean_numeric_value("10.5") == 10.5 |
|
|
|
|
|
def test_currency_format(self): |
|
|
"""Test with currency formatted values.""" |
|
|
assert clean_numeric_value("$10.50") == 10.5 |
|
|
assert clean_numeric_value("$1,234.56") == 1234.56 |
|
|
assert clean_numeric_value("($10.50)") == -10.5 |
|
|
|
|
|
def test_none_value(self): |
|
|
"""Test with None value.""" |
|
|
with pytest.raises(ValueError, match="Value is NaN or None"): |
|
|
clean_numeric_value(None) |
|
|
|
|
|
|
|
|
assert clean_numeric_value(None, default=0) == 0 |
|
|
|
|
|
def test_nan_value(self): |
|
|
"""Test with NaN value.""" |
|
|
with pytest.raises(ValueError, match="Value is NaN or None"): |
|
|
clean_numeric_value(float("nan")) |
|
|
|
|
|
|
|
|
assert clean_numeric_value(float("nan"), default=0) == 0 |
|
|
|
|
|
def test_invalid_format(self): |
|
|
"""Test with invalid format.""" |
|
|
with pytest.raises(ValueError, match="Could not convert"): |
|
|
clean_numeric_value("not a number") |
|
|
|
|
|
|
|
|
assert clean_numeric_value("not a number", default=0) == 0 |
|
|
|
|
|
def test_zero_constraint(self): |
|
|
"""Test with zero constraint.""" |
|
|
with pytest.raises(ValueError, match="Zero value not allowed"): |
|
|
clean_numeric_value(0, allow_zero=False) |
|
|
|
|
|
|
|
|
assert clean_numeric_value(0, allow_zero=False, default=1) == 1 |
|
|
|
|
|
def test_negative_constraint(self): |
|
|
"""Test with negative constraint.""" |
|
|
with pytest.raises(ValueError, match="Negative value not allowed"): |
|
|
clean_numeric_value(-10, allow_negative=False) |
|
|
|
|
|
|
|
|
assert clean_numeric_value(-10, allow_negative=False, default=10) == 10 |
|
|
|