Files
redash/tests/utils/test_parameterized_query.py
Omer Lachish 0d76c036cb Be more permissive when parameters are safe (#3383)
* use the textless endpoint (/api/queries/:id/results) for pristine
queriest

* reverse conditional. not not is making me the headaches.

* add ParameterizedQuery#is_safe with an inital naive implementation which
treats any query with a text parameter as not safe. This will be
remedied later when DB drivers will handle these parameters.

* allow getting new query results even if user has only view permissions
to the data source (given that the query is safe)

* fix lint error - getDerivedStateFromProps should be placed after state

* Revert "use the textless endpoint (/api/queries/:id/results) for pristine"

This reverts commit cd2cee7738.

* move execution preparation to a different function, which will be soon
reused

* go to textless /api/queries/:id/results by default

* let the query view decide if text or textless endpoint is needed

* allow safe queries to be executed in the UI even if the user has no
permission to execute and create new query results

* change `run_query`'s signature to accept a ParameterizedQuery instead of
constructing it inside

* use dict#get instead of a None guard

* use ParameterizedQuery in queries handler as well

* test that /queries/:id/results allows execution of safe queries even if
user has view_only permissions

* lint

* raise HTTP 400 when receiving invalid parameter values. Fixes #3394

* remove unused methods

* avoid cyclic imports by importing only when needed

* verify that a ParameterizedQuery without any parameters is considered
safe

* introduce query.parameter_schema

* encapsulate ParameterizedQuery creation inside Query
2019-02-26 20:55:01 +02:00

207 lines
7.8 KiB
Python

from unittest import TestCase
from mock import patch
from collections import namedtuple
import pytest
from redash.utils.parameterized_query import ParameterizedQuery, InvalidParameterError, dropdown_values
class TestParameterizedQuery(TestCase):
def test_returns_empty_list_for_regular_query(self):
query = ParameterizedQuery(u"SELECT 1")
self.assertEqual(set([]), query.missing_params)
def test_finds_all_params_when_missing(self):
query = ParameterizedQuery(u"SELECT {{param}} FROM {{table}}")
self.assertEqual(set(['param', 'table']), query.missing_params)
def test_finds_all_params(self):
query = ParameterizedQuery(u"SELECT {{param}} FROM {{table}}").apply({
'param': 'value',
'table': 'value'
})
self.assertEqual(set([]), query.missing_params)
def test_deduplicates_params(self):
query = ParameterizedQuery(u"SELECT {{param}}, {{param}} FROM {{table}}").apply({
'param': 'value',
'table': 'value'
})
self.assertEqual(set([]), query.missing_params)
def test_handles_nested_params(self):
query = ParameterizedQuery(u"SELECT {{param}}, {{param}} FROM {{table}} -- {{#test}} {{nested_param}} {{/test}}").apply({
'param': 'value',
'table': 'value'
})
self.assertEqual(set(['test', 'nested_param']), query.missing_params)
def test_handles_objects(self):
query = ParameterizedQuery(u"SELECT * FROM USERS WHERE created_at between '{{ created_at.start }}' and '{{ created_at.end }}'").apply({
'created_at': {
'start': 1,
'end': 2
}
})
self.assertEqual(set([]), query.missing_params)
def test_raises_on_parameters_not_in_schema(self):
schema = [{"name": "bar", "type": "text"}]
query = ParameterizedQuery("foo", schema)
with pytest.raises(InvalidParameterError):
query.apply({"qux": 7})
def test_raises_on_invalid_text_parameters(self):
schema = [{"name": "bar", "type": "text"}]
query = ParameterizedQuery("foo", schema)
with pytest.raises(InvalidParameterError):
query.apply({"bar": 7})
def test_validates_text_parameters(self):
schema = [{"name": "bar", "type": "text"}]
query = ParameterizedQuery("foo {{bar}}", schema)
query.apply({"bar": u"baz"})
self.assertEquals("foo baz", query.text)
def test_raises_on_invalid_number_parameters(self):
schema = [{"name": "bar", "type": "number"}]
query = ParameterizedQuery("foo", schema)
with pytest.raises(InvalidParameterError):
query.apply({"bar": "baz"})
def test_validates_number_parameters(self):
schema = [{"name": "bar", "type": "number"}]
query = ParameterizedQuery("foo {{bar}}", schema)
query.apply({"bar": 7})
self.assertEquals("foo 7", query.text)
def test_raises_on_invalid_date_parameters(self):
schema = [{"name": "bar", "type": "date"}]
query = ParameterizedQuery("foo", schema)
with pytest.raises(InvalidParameterError):
query.apply({"bar": "baz"})
def test_validates_date_parameters(self):
schema = [{"name": "bar", "type": "date"}]
query = ParameterizedQuery("foo {{bar}}", schema)
query.apply({"bar": "2000-01-01 12:00:00"})
self.assertEquals("foo 2000-01-01 12:00:00", query.text)
def test_raises_on_invalid_enum_parameters(self):
schema = [{"name": "bar", "type": "enum", "enumOptions": ["baz", "qux"]}]
query = ParameterizedQuery("foo", schema)
with pytest.raises(InvalidParameterError):
query.apply({"bar": 7})
def test_raises_on_unlisted_enum_value_parameters(self):
schema = [{"name": "bar", "type": "enum", "enumOptions": ["baz", "qux"]}]
query = ParameterizedQuery("foo", schema)
with pytest.raises(InvalidParameterError):
query.apply({"bar": "shlomo"})
def test_validates_enum_parameters(self):
schema = [{"name": "bar", "type": "enum", "enumOptions": ["baz", "qux"]}]
query = ParameterizedQuery("foo {{bar}}", schema)
query.apply({"bar": "baz"})
self.assertEquals("foo baz", query.text)
@patch('redash.utils.parameterized_query.dropdown_values')
def test_raises_on_invalid_query_parameters(self, _):
schema = [{"name": "bar", "type": "query", "queryId": 1}]
query = ParameterizedQuery("foo", schema)
with pytest.raises(InvalidParameterError):
query.apply({"bar": 7})
@patch('redash.utils.parameterized_query.dropdown_values', return_value=[{"value": "baz"}])
def test_raises_on_unlisted_query_value_parameters(self, _):
schema = [{"name": "bar", "type": "query", "queryId": 1}]
query = ParameterizedQuery("foo", schema)
with pytest.raises(InvalidParameterError):
query.apply({"bar": "shlomo"})
@patch('redash.utils.parameterized_query.dropdown_values', return_value=[{"value": "baz"}])
def test_validates_query_parameters(self, _):
schema = [{"name": "bar", "type": "query", "queryId": 1}]
query = ParameterizedQuery("foo {{bar}}", schema)
query.apply({"bar": "baz"})
self.assertEquals("foo baz", query.text)
def test_raises_on_invalid_date_range_parameters(self):
schema = [{"name": "bar", "type": "date-range"}]
query = ParameterizedQuery("foo", schema)
with pytest.raises(InvalidParameterError):
query.apply({"bar": "baz"})
def test_validates_date_range_parameters(self):
schema = [{"name": "bar", "type": "date-range"}]
query = ParameterizedQuery("foo {{bar.start}} {{bar.end}}", schema)
query.apply({"bar": {"start": "2000-01-01 12:00:00", "end": "2000-12-31 12:00:00"}})
self.assertEquals("foo 2000-01-01 12:00:00 2000-12-31 12:00:00", query.text)
def test_raises_on_unexpected_param_types(self):
schema = [{"name": "bar", "type": "burrito"}]
query = ParameterizedQuery("foo", schema)
with pytest.raises(InvalidParameterError):
query.apply({"bar": "baz"})
def test_is_not_safe_if_expecting_text_parameter(self):
schema = [{"name": "bar", "type": "text"}]
query = ParameterizedQuery("foo", schema)
self.assertFalse(query.is_safe)
def test_is_safe_if_not_expecting_text_parameter(self):
schema = [{"name": "bar", "type": "number"}]
query = ParameterizedQuery("foo", schema)
self.assertTrue(query.is_safe)
def test_is_safe_if_not_expecting_any_parameters(self):
schema = []
query = ParameterizedQuery("foo", schema)
self.assertTrue(query.is_safe)
@patch('redash.utils.parameterized_query._load_result', return_value={
"columns": [{"name": "id"}, {"name": "Name"}, {"name": "Value"}],
"rows": [{"id": 5, "Name": "John", "Value": "John Doe"}]})
def test_dropdown_values_prefers_name_and_value_columns(self, _):
values = dropdown_values(1)
self.assertEquals(values, [{"name": "John", "value": "John Doe"}])
@patch('redash.utils.parameterized_query._load_result', return_value={
"columns": [{"name": "id"}, {"name": "fish"}, {"name": "poultry"}],
"rows": [{"fish": "Clown", "id": 5, "poultry": "Hen"}]})
def test_dropdown_values_compromises_for_first_column(self, _):
values = dropdown_values(1)
self.assertEquals(values, [{"name": 5, "value": 5}])
@patch('redash.utils.parameterized_query._load_result', return_value={
"columns": [{"name": "ID"}, {"name": "fish"}, {"name": "poultry"}],
"rows": [{"fish": "Clown", "ID": 5, "poultry": "Hen"}]})
def test_dropdown_supports_upper_cased_columns(self, _):
values = dropdown_values(1)
self.assertEquals(values, [{"name": 5, "value": 5}])