Coverage for tests/test_data_row.py: 100%
65 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-05 04:36 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-05 04:36 +0000
1import pandas as pd
2import pytest
4from sankey_cashflow import DataRow
7@pytest.fixture
8def valid_row():
9 return [
10 pd.to_datetime('2023-01-01'),
11 'Groceries',
12 'Grocery Store',
13 'food,essentials',
14 'Monthly groceries',
15 'Income',
16 'Groceries',
17 'computed',
18 0,
19 100.0,
20 5.0,
21 2.0,
22 'Expense'
23 ]
26class TestValidate:
28 def test_validate_valid_data(self, valid_row):
29 assert DataRow.validate(valid_row, include_classifications=True) == valid_row
31 def test_validate_invalid_amount_raises(self, valid_row):
32 invalid_row = list(valid_row)
33 invalid_row[9] = 'invalid_amount'
34 with pytest.raises(Exception):
35 DataRow.validate(invalid_row, include_classifications=True)
37 def test_validate_non_nullable_field_raises(self, valid_row):
38 invalid_row = list(valid_row)
39 invalid_row[0] = None # Date is required, non-nullable
40 with pytest.raises(Exception):
41 DataRow.validate(invalid_row, include_classifications=True)
43 def test_validate_wrong_column_count_raises(self, valid_row):
44 with pytest.raises(Exception):
45 DataRow.validate(valid_row[:-1], include_classifications=True)
47 def test_validate_disallowed_type_value_raises(self, valid_row):
48 invalid_row = list(valid_row)
49 invalid_row[7] = 'not_a_real_type'
50 with pytest.raises(Exception):
51 DataRow.validate(invalid_row, include_classifications=True)
53 def test_validate_header_only_correct_order(self):
54 header = ['Date', 'Category', 'Description', 'Tags', 'Comments', 'Source', 'Target',
55 'Type', 'Distribution', 'Amount', 'Sales Tax', 'Tips']
56 result, error = DataRow.validate(header, header_only=True)
57 assert result is True
58 assert error is None
60 def test_validate_header_only_wrong_order(self):
61 header = ['Category', 'Date', 'Description', 'Tags', 'Comments', 'Source', 'Target',
62 'Type', 'Distribution', 'Amount', 'Sales Tax', 'Tips']
63 result, error = DataRow.validate(header, header_only=True)
64 assert result is False
65 assert error is not None
68class TestCreate:
70 def test_create_matches_manually_built_row(self, valid_row):
71 created = DataRow.create(
72 date=pd.to_datetime('2023-01-01'),
73 category_name='Groceries',
74 source='Income',
75 target='Groceries',
76 amount=100.0,
77 description='Grocery Store',
78 sales_tax=5.0,
79 tips=2.0,
80 comment='Monthly groceries',
81 tags='food,essentials',
82 row_type='computed',
83 distribution=0,
84 classification='Expense'
85 )
86 assert created == valid_row
88 def test_create_coerces_string_amount(self):
89 created = DataRow.create(
90 date=pd.to_datetime('2023-01-01'), category_name='Groceries',
91 source='Income', target='Groceries', amount='100.0'
92 )
93 assert created[9] == 100.0
95 def test_create_unparseable_amount_defaults_to_zero(self):
96 created = DataRow.create(
97 date=pd.to_datetime('2023-01-01'), category_name='Groceries',
98 source='Income', target='Groceries', amount='not_a_number'
99 )
100 assert created[9] == 0
102 def test_create_null_tips_defaults_to_zero(self):
103 created = DataRow.create(
104 date=pd.to_datetime('2023-01-01'), category_name='Groceries',
105 source='Income', target='Groceries', amount=100.0, tips=None
106 )
107 assert created[11] == 0
109 def test_create_null_distribution_defaults_to_zero(self):
110 created = DataRow.create(
111 date=pd.to_datetime('2023-01-01'), category_name='Groceries',
112 source='Income', target='Groceries', amount=100.0, distribution=None
113 )
114 assert created[8] == 0
116 def test_create_null_sales_tax_defaults_to_zero(self):
117 created = DataRow.create(
118 date=pd.to_datetime('2023-01-01'), category_name='Groceries',
119 source='Income', target='Groceries', amount=100.0, sales_tax=None
120 )
121 assert created[10] == 0
124class TestTagMatches:
126 def test_returns_matching_tags(self):
127 assert DataRow.tag_matches('food,essentials', ['food', 'luxury']) == ['food']
129 def test_no_matches_returns_empty_list(self):
130 assert DataRow.tag_matches('food,essentials', ['luxury']) == []
132 def test_no_search_tags_returns_none(self):
133 assert DataRow.tag_matches('food,essentials', None) is None
135 def test_no_row_tags_returns_none(self):
136 assert DataRow.tag_matches(None, ['food']) is None