File size: 10,780 Bytes
263637f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""Integration tests for datasource validate_expression API endpoint"""

from unittest.mock import patch

import pytest

from superset.utils import json
from superset.utils.core import SqlExpressionType
from tests.integration_tests.base_tests import SupersetTestCase

# Note: Tests use mocked responses, so we don't need the actual energy table fixture


@pytest.mark.skip(
    reason=(
        "TODO: Fix test class to work with DuckDB example data format. "
        "Birth names fixture conflicts with new example data structure."
    )
)
class TestDatasourceValidateExpressionApi(SupersetTestCase):
    """Test the datasource validate_expression API endpoint"""

    @patch("superset.connectors.sqla.models.SqlaTable.validate_expression")
    def test_validate_expression_column_success(self, mock_validate):
        """Test successful validation of a column expression"""
        self.login("admin")

        # Mock successful validation
        mock_validate.return_value = {"valid": True, "errors": []}

        # Use the birth_names dataset for testing
        datasource = self.get_birth_names_dataset()
        datasource_id = datasource.id

        rv = self.client.post(
            f"/api/v1/datasource/table/{datasource_id}/validate_expression/",
            json={
                "expression": "test_col",
                "expression_type": SqlExpressionType.COLUMN.value,
            },
        )

        assert rv.status_code == 200
        data = json.loads(rv.data.decode("utf-8"))
        assert "result" in data
        assert data["result"] == []  # Empty array means success

    @patch("superset.connectors.sqla.models.SqlaTable.validate_expression")
    def test_validate_expression_metric_success(self, mock_validate):
        """Test successful validation of a metric expression"""
        self.login("admin")

        # Mock successful validation
        mock_validate.return_value = {"valid": True, "errors": []}

        # Use the birth_names dataset for testing
        datasource = self.get_birth_names_dataset()
        datasource_id = datasource.id

        rv = self.client.post(
            f"/api/v1/datasource/table/{datasource_id}/validate_expression/",
            json={
                "expression": "SUM(amount)",
                "expression_type": SqlExpressionType.METRIC.value,
            },
        )

        assert rv.status_code == 200
        data = json.loads(rv.data.decode("utf-8"))
        assert "result" in data
        assert data["result"] == []

    @patch("superset.connectors.sqla.models.SqlaTable.validate_expression")
    def test_validate_expression_where_success(self, mock_validate):
        """Test successful validation of a WHERE clause expression"""
        self.login("admin")

        # Mock successful validation
        mock_validate.return_value = {"valid": True, "errors": []}

        # Use the birth_names dataset for testing
        datasource = self.get_birth_names_dataset()
        datasource_id = datasource.id

        rv = self.client.post(
            f"/api/v1/datasource/table/{datasource_id}/validate_expression/",
            json={
                "expression": "status = 'active'",
                "expression_type": SqlExpressionType.WHERE.value,
            },
        )

        assert rv.status_code == 200
        data = json.loads(rv.data.decode("utf-8"))
        assert "result" in data
        assert data["result"] == []

    @patch("superset.connectors.sqla.models.SqlaTable.validate_expression")
    def test_validate_expression_having_success(self, mock_validate):
        """Test successful validation of a HAVING clause expression"""
        self.login("admin")

        # Mock successful validation
        mock_validate.return_value = {"valid": True, "errors": []}

        # Use the birth_names dataset for testing
        datasource = self.get_birth_names_dataset()
        datasource_id = datasource.id

        rv = self.client.post(
            f"/api/v1/datasource/table/{datasource_id}/validate_expression/",
            json={
                "expression": "SUM(amount) > 100",
                "expression_type": SqlExpressionType.HAVING.value,
            },
        )

        assert rv.status_code == 200
        data = json.loads(rv.data.decode("utf-8"))
        assert "result" in data
        assert data["result"] == []

    def test_validate_expression_invalid_sql(self):
        """Test validation of invalid SQL expression"""
        self.login("admin")

        # Use the birth_names dataset for testing
        datasource = self.get_birth_names_dataset()
        datasource_id = datasource.id

        with patch(
            "superset.connectors.sqla.models.SqlaTable.validate_expression"
        ) as mock_validate:
            mock_validate.return_value = {
                "valid": False,
                "errors": [{"message": "Invalid SQL syntax"}],
            }

            rv = self.client.post(
                f"/api/v1/datasource/table/{datasource_id}/validate_expression/",
                json={
                    "expression": "INVALID SQL HERE",
                    "expression_type": SqlExpressionType.COLUMN.value,
                },
            )

            assert rv.status_code == 200
            data = json.loads(rv.data.decode("utf-8"))
            assert "result" in data
            assert len(data["result"]) == 1
            assert data["result"][0]["message"] == "Invalid SQL syntax"

    def test_validate_expression_having_with_non_aggregated_column(self):
        """Test that HAVING clause fails for non-aggregated columns"""
        self.login("admin")

        # Use the birth_names dataset for testing
        datasource = self.get_birth_names_dataset()
        datasource_id = datasource.id

        with patch(
            "superset.connectors.sqla.models.SqlaTable.validate_expression"
        ) as mock_validate:
            mock_validate.return_value = {
                "valid": False,
                "errors": [
                    {
                        "message": (
                            "column 'source' must appear in the GROUP BY clause "
                            "or be used in an aggregate function"
                        )
                    }
                ],
            }

            rv = self.client.post(
                f"/api/v1/datasource/table/{datasource_id}/validate_expression/",
                json={
                    "expression": "source = 'energy_source1'",
                    "expression_type": SqlExpressionType.HAVING.value,
                },
            )

            assert rv.status_code == 200
            data = json.loads(rv.data.decode("utf-8"))
            assert "result" in data
            assert len(data["result"]) == 1
            assert "must appear in the GROUP BY clause" in data["result"][0]["message"]

    def test_validate_expression_empty(self):
        """Test validation of empty expression"""
        self.login("admin")

        # Use the birth_names dataset for testing
        datasource = self.get_birth_names_dataset()
        datasource_id = datasource.id

        rv = self.client.post(
            f"/api/v1/datasource/table/{datasource_id}/validate_expression/",
            json={
                "expression": "",
                "expression_type": SqlExpressionType.COLUMN.value,
            },
        )

        assert rv.status_code == 400  # Bad request for empty expression

    def test_validate_expression_missing_parameters(self):
        """Test validation with missing required parameters"""
        self.login("admin")

        # Use the birth_names dataset for testing
        datasource = self.get_birth_names_dataset()
        datasource_id = datasource.id

        # Missing expression_type - defaults to "where"
        rv = self.client.post(
            f"/api/v1/datasource/table/{datasource_id}/validate_expression/",
            json={"expression": "test_col"},
        )

        assert rv.status_code == 200  # Defaults to "where" type, so succeeds

        # Missing expression - this should fail
        rv = self.client.post(
            f"/api/v1/datasource/table/{datasource_id}/validate_expression/",
            json={"expression_type": SqlExpressionType.COLUMN.value},
        )

        assert rv.status_code == 400  # Missing expression is an error

    def test_validate_expression_datasource_not_found(self):
        """Test validation with non-existent datasource"""
        self.login("admin")

        rv = self.client.post(
            "/api/v1/datasource/table/99999/validate_expression/",
            json={
                "expression": "test_col",
                "expression_type": SqlExpressionType.COLUMN.value,
            },
        )

        assert rv.status_code == 404

    def test_validate_expression_no_permission(self):
        """Test validation without permission to access datasource"""
        # Create a user without admin privileges
        self.login("gamma")

        # Use the birth_names dataset for testing
        datasource = self.get_birth_names_dataset()
        datasource_id = datasource.id

        rv = self.client.post(
            f"/api/v1/datasource/table/{datasource_id}/validate_expression/",
            json={
                "expression": "test_col",
                "expression_type": SqlExpressionType.COLUMN.value,
            },
        )

        # Should get 403 Forbidden or 404 if datasource is hidden
        assert rv.status_code in [403, 404]

    def test_validate_expression_invalid_datasource_type(self):
        """Test validation with invalid datasource type"""
        self.login("admin")

        rv = self.client.post(
            "/api/v1/datasource/invalid_type/1/validate_expression/",
            json={
                "expression": "test_col",
                "expression_type": SqlExpressionType.COLUMN.value,
            },
        )

        assert rv.status_code == 400  # Returns 400 for invalid datasource type