Skip to content

Commit 5b992bb

Browse files
committed
test: add coverage for dynamic currency detection feature
1 parent b5706ec commit 5b992bb

File tree

5 files changed

+379
-0
lines changed

5 files changed

+379
-0
lines changed

tests/integration_tests/viz_tests.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,70 @@ def test_cache_timeout(self):
179179
data_cache_timeout
180180
)
181181

182+
def test_detect_currency_returns_none_when_currency_format_not_dict(self):
183+
"""Test _detect_currency returns None when currency_format is not a dict."""
184+
datasource = self.get_datasource_mock()
185+
datasource.currency_code_column = "currency_code"
186+
form_data = {"currency_format": "invalid"}
187+
test_viz = viz.BaseViz(datasource, form_data)
188+
189+
result = test_viz._detect_currency()
190+
191+
assert result is None
192+
193+
def test_detect_currency_returns_none_when_symbol_not_auto(self):
194+
"""Test _detect_currency returns None when symbol is not AUTO."""
195+
datasource = self.get_datasource_mock()
196+
datasource.currency_code_column = "currency_code"
197+
form_data = {"currency_format": {"symbol": "USD"}}
198+
test_viz = viz.BaseViz(datasource, form_data)
199+
200+
result = test_viz._detect_currency()
201+
202+
assert result is None
203+
204+
def test_detect_currency_returns_none_when_no_currency_column(self):
205+
"""Test _detect_currency returns None when datasource has no currency column."""
206+
datasource = self.get_datasource_mock()
207+
datasource.currency_code_column = None
208+
form_data = {"currency_format": {"symbol": "AUTO"}}
209+
test_viz = viz.BaseViz(datasource, form_data)
210+
211+
result = test_viz._detect_currency()
212+
213+
assert result is None
214+
215+
@patch("superset.viz.detect_currency_from_df")
216+
def test_detect_currency_uses_dataframe_when_column_present(
217+
self, mock_detect_from_df
218+
):
219+
"""Test _detect_currency uses df when currency column is present."""
220+
datasource = self.get_datasource_mock()
221+
datasource.currency_code_column = "currency_code"
222+
form_data = {"currency_format": {"symbol": "AUTO"}}
223+
test_viz = viz.BaseViz(datasource, form_data)
224+
df = pd.DataFrame({"currency_code": ["USD", "USD"]})
225+
mock_detect_from_df.return_value = "USD"
226+
227+
result = test_viz._detect_currency(df=df)
228+
229+
assert result == "USD"
230+
mock_detect_from_df.assert_called_once_with(df, "currency_code")
231+
232+
@patch("superset.viz.detect_currency")
233+
def test_detect_currency_queries_datasource_when_no_df(self, mock_detect):
234+
"""Test _detect_currency queries datasource when df is None."""
235+
datasource = self.get_datasource_mock()
236+
datasource.currency_code_column = "currency_code"
237+
form_data = {"currency_format": {"symbol": "AUTO"}}
238+
test_viz = viz.BaseViz(datasource, form_data)
239+
mock_detect.return_value = "EUR"
240+
241+
result = test_viz._detect_currency()
242+
243+
assert result == "EUR"
244+
mock_detect.assert_called_once()
245+
182246

183247
class TestPairedTTest(SupersetTestCase):
184248
def test_get_data_transforms_dataframe(self):
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
from unittest.mock import MagicMock, patch
18+
19+
import pandas as pd
20+
import pytest
21+
22+
from superset.common.query_actions import _detect_currency
23+
24+
25+
@pytest.fixture
26+
def mock_query_context() -> MagicMock:
27+
"""Create a mock QueryContext with AUTO currency format."""
28+
context = MagicMock()
29+
context.form_data = {"currency_format": {"symbol": "AUTO"}}
30+
return context
31+
32+
33+
@pytest.fixture
34+
def mock_query_obj() -> MagicMock:
35+
"""Create a mock QueryObject with filter attributes."""
36+
obj = MagicMock()
37+
obj.filter = []
38+
obj.granularity = None
39+
obj.from_dttm = None
40+
obj.to_dttm = None
41+
obj.extras = {}
42+
return obj
43+
44+
45+
@pytest.fixture
46+
def mock_datasource() -> MagicMock:
47+
"""Create a mock datasource with currency column."""
48+
ds = MagicMock()
49+
ds.currency_code_column = "currency_code"
50+
return ds
51+
52+
53+
def test_detect_currency_returns_none_when_form_data_is_none(
54+
mock_query_obj: MagicMock,
55+
mock_datasource: MagicMock,
56+
) -> None:
57+
"""Returns None when query context has no form_data."""
58+
context = MagicMock()
59+
context.form_data = None
60+
61+
result = _detect_currency(context, mock_query_obj, mock_datasource)
62+
63+
assert result is None
64+
65+
66+
def test_detect_currency_returns_none_when_currency_format_not_dict(
67+
mock_query_obj: MagicMock,
68+
mock_datasource: MagicMock,
69+
) -> None:
70+
"""Returns None when currency_format is not a dict."""
71+
context = MagicMock()
72+
context.form_data = {"currency_format": "invalid"}
73+
74+
result = _detect_currency(context, mock_query_obj, mock_datasource)
75+
76+
assert result is None
77+
78+
79+
def test_detect_currency_returns_none_when_symbol_not_auto(
80+
mock_query_obj: MagicMock,
81+
mock_datasource: MagicMock,
82+
) -> None:
83+
"""Returns None when currency_format.symbol is not AUTO."""
84+
context = MagicMock()
85+
context.form_data = {"currency_format": {"symbol": "USD"}}
86+
87+
result = _detect_currency(context, mock_query_obj, mock_datasource)
88+
89+
assert result is None
90+
91+
92+
def test_detect_currency_returns_none_when_no_currency_column(
93+
mock_query_context: MagicMock,
94+
mock_query_obj: MagicMock,
95+
) -> None:
96+
"""Returns None when datasource has no currency_code_column."""
97+
datasource = MagicMock()
98+
datasource.currency_code_column = None
99+
100+
result = _detect_currency(mock_query_context, mock_query_obj, datasource)
101+
102+
assert result is None
103+
104+
105+
@patch("superset.common.query_actions.detect_currency_from_df")
106+
def test_detect_currency_uses_dataframe_when_column_present(
107+
mock_detect_from_df: MagicMock,
108+
mock_query_context: MagicMock,
109+
mock_query_obj: MagicMock,
110+
mock_datasource: MagicMock,
111+
) -> None:
112+
"""Uses detect_currency_from_df when df contains currency column."""
113+
df = pd.DataFrame({"currency_code": ["USD", "USD"]})
114+
mock_detect_from_df.return_value = "USD"
115+
116+
result = _detect_currency(mock_query_context, mock_query_obj, mock_datasource, df)
117+
118+
assert result == "USD"
119+
mock_detect_from_df.assert_called_once_with(df, "currency_code")
120+
121+
122+
@patch("superset.common.query_actions.detect_currency")
123+
def test_detect_currency_queries_datasource_when_no_df(
124+
mock_detect: MagicMock,
125+
mock_query_context: MagicMock,
126+
mock_query_obj: MagicMock,
127+
mock_datasource: MagicMock,
128+
) -> None:
129+
"""Queries datasource when df is None."""
130+
mock_detect.return_value = "EUR"
131+
132+
result = _detect_currency(mock_query_context, mock_query_obj, mock_datasource)
133+
134+
assert result == "EUR"
135+
mock_detect.assert_called_once_with(
136+
datasource=mock_datasource,
137+
filters=mock_query_obj.filter,
138+
granularity=mock_query_obj.granularity,
139+
from_dttm=mock_query_obj.from_dttm,
140+
to_dttm=mock_query_obj.to_dttm,
141+
extras=mock_query_obj.extras,
142+
)
143+
144+
145+
@patch("superset.common.query_actions.detect_currency")
146+
def test_detect_currency_queries_datasource_when_column_not_in_df(
147+
mock_detect: MagicMock,
148+
mock_query_context: MagicMock,
149+
mock_query_obj: MagicMock,
150+
mock_datasource: MagicMock,
151+
) -> None:
152+
"""Falls back to query when df doesn't have currency column."""
153+
df = pd.DataFrame({"other_column": ["value"]})
154+
mock_detect.return_value = "GBP"
155+
156+
result = _detect_currency(mock_query_context, mock_query_obj, mock_datasource, df)
157+
158+
assert result == "GBP"
159+
mock_detect.assert_called_once()

tests/unit_tests/common/test_query_context_factory.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,97 @@ def test_apply_filters_no_temporal_filters(self):
436436
self.factory._apply_filters(query_object)
437437

438438
assert query_object.filter[0]["val"] == "value"
439+
440+
def test_add_currency_column_no_form_data(self):
441+
"""Test _add_currency_column when form_data is None."""
442+
query_object = Mock(spec=QueryObject)
443+
query_object.columns = ["col1"]
444+
datasource = Mock()
445+
446+
self.factory._add_currency_column(query_object, None, datasource)
447+
448+
assert query_object.columns == ["col1"]
449+
450+
def test_add_currency_column_no_columns(self):
451+
"""Test _add_currency_column when query_object has no columns."""
452+
query_object = Mock(spec=QueryObject)
453+
query_object.columns = []
454+
form_data = {
455+
"viz_type": "pivot_table_v2",
456+
"currency_format": {"symbol": "AUTO"},
457+
}
458+
datasource = Mock()
459+
datasource.currency_code_column = "currency_code"
460+
461+
self.factory._add_currency_column(query_object, form_data, datasource)
462+
463+
assert query_object.columns == []
464+
465+
def test_add_currency_column_unsupported_viz_type(self):
466+
"""Test _add_currency_column with unsupported viz type."""
467+
query_object = Mock(spec=QueryObject)
468+
query_object.columns = ["col1"]
469+
form_data = {"viz_type": "pie", "currency_format": {"symbol": "AUTO"}}
470+
datasource = Mock()
471+
datasource.currency_code_column = "currency_code"
472+
473+
self.factory._add_currency_column(query_object, form_data, datasource)
474+
475+
assert query_object.columns == ["col1"]
476+
477+
def test_add_currency_column_symbol_not_auto(self):
478+
"""Test _add_currency_column when symbol is not AUTO."""
479+
query_object = Mock(spec=QueryObject)
480+
query_object.columns = ["col1"]
481+
form_data = {"viz_type": "pivot_table_v2", "currency_format": {"symbol": "USD"}}
482+
datasource = Mock()
483+
datasource.currency_code_column = "currency_code"
484+
485+
self.factory._add_currency_column(query_object, form_data, datasource)
486+
487+
assert query_object.columns == ["col1"]
488+
489+
def test_add_currency_column_no_currency_column_on_datasource(self):
490+
"""Test _add_currency_column when datasource has no currency column."""
491+
query_object = Mock(spec=QueryObject)
492+
query_object.columns = ["col1"]
493+
form_data = {
494+
"viz_type": "pivot_table_v2",
495+
"currency_format": {"symbol": "AUTO"},
496+
}
497+
datasource = Mock()
498+
datasource.currency_code_column = None
499+
500+
self.factory._add_currency_column(query_object, form_data, datasource)
501+
502+
assert query_object.columns == ["col1"]
503+
504+
def test_add_currency_column_already_in_query(self):
505+
"""Test _add_currency_column when currency column already exists."""
506+
query_object = Mock(spec=QueryObject)
507+
query_object.columns = ["col1", "currency_code"]
508+
form_data = {
509+
"viz_type": "pivot_table_v2",
510+
"currency_format": {"symbol": "AUTO"},
511+
}
512+
datasource = Mock()
513+
datasource.currency_code_column = "currency_code"
514+
515+
self.factory._add_currency_column(query_object, form_data, datasource)
516+
517+
assert query_object.columns == ["col1", "currency_code"]
518+
519+
def test_add_currency_column_adds_column_for_supported_viz_types(self):
520+
"""Test _add_currency_column adds column for supported viz types"""
521+
for viz_type in ["pivot_table_v2", "table"]:
522+
query_object = Mock(spec=QueryObject)
523+
query_object.columns = ["col1"]
524+
form_data = {"viz_type": viz_type, "currency_format": {"symbol": "AUTO"}}
525+
datasource = Mock()
526+
datasource.currency_code_column = "currency_code"
527+
528+
self.factory._add_currency_column(query_object, form_data, datasource)
529+
530+
assert query_object.columns == ["col1", "currency_code"], (
531+
f"Failed for {viz_type}"
532+
)

tests/unit_tests/connectors/sqla/models_test.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,3 +855,54 @@ def test_sqla_table_currency_code_column_property() -> None:
855855
currency_code_column="currency",
856856
)
857857
assert table.currency_code_column == "currency"
858+
859+
860+
def test_sqla_table_data_includes_currency_code_column(mocker: MockerFixture) -> None:
861+
"""
862+
Test that data property includes currency_code_column.
863+
"""
864+
database = mocker.MagicMock()
865+
database.get_sqla_engine.return_value.__enter__ = mocker.MagicMock()
866+
database.get_sqla_engine.return_value.__exit__ = mocker.MagicMock()
867+
868+
table = SqlaTable(
869+
table_name="sales",
870+
database=database,
871+
currency_code_column="currency_code",
872+
main_dttm_col="ds",
873+
)
874+
table.columns = []
875+
table.metrics = []
876+
877+
# Mock the columns property to return empty list
878+
mocker.patch.object(SqlaTable, "columns", [])
879+
mocker.patch.object(SqlaTable, "metrics", [])
880+
881+
data = table.data
882+
assert data["currency_code_column"] == "currency_code"
883+
assert data["main_dttm_col"] == "ds"
884+
885+
886+
def test_sqla_table_link_escapes_url(mocker: MockerFixture) -> None:
887+
"""
888+
Test that link property properly escapes URL to prevent XSS.
889+
"""
890+
database = Database(database_name="my_db")
891+
table = SqlaTable(
892+
table_name='test<script>alert("xss")</script>',
893+
database=database,
894+
id=1,
895+
)
896+
897+
# Mock explore_url to return a URL with special characters
898+
mocker.patch.object(
899+
SqlaTable,
900+
"explore_url",
901+
new_callable=mocker.PropertyMock,
902+
return_value='/explore/?datasource_type=table&datasource_id=1&name=<script>alert("xss")</script>',
903+
)
904+
905+
link = table.link
906+
# Verify that special characters are escaped in both name and URL
907+
assert "&lt;script&gt;" in str(link)
908+
assert "<script>" not in str(link)

tests/unit_tests/utils/currency_test.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,14 @@ def test_detect_currency_from_df_ignores_null_values() -> None:
216216
result = detect_currency_from_df(df, "currency_code")
217217

218218
assert result == "USD"
219+
220+
221+
def test_detect_currency_returns_none_when_query_not_callable() -> None:
222+
"""Returns None when datasource query attribute is not callable."""
223+
datasource = MagicMock()
224+
datasource.currency_code_column = "currency_code"
225+
datasource.query = "not_a_callable" # Set to a string instead of a method
226+
227+
result = detect_currency(datasource)
228+
229+
assert result is None

0 commit comments

Comments
 (0)