# 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. # pylint: disable=import-outside-toplevel, invalid-name, unused-argument, redefined-outer-name import pytest from marshmallow import ValidationError from superset.datasets.schemas import validate_python_date_format # pylint: disable=too-few-public-methods @pytest.mark.parametrize( "payload", [ "epoch_ms", "epoch_s", "%Y-%m-%dT%H:%M:%S.%f", "%Y%m%d", ], ) def test_validate_python_date_format(payload) -> None: assert validate_python_date_format(payload) @pytest.mark.parametrize( "payload", [ "%d%m%Y", "%Y/%m/%dT%H:%M:%S.%f", ], ) def test_validate_python_date_format_raises(payload) -> None: with pytest.raises(ValidationError): validate_python_date_format(payload) def test_drill_info_user_schema_does_not_expose_email() -> None: """ Regression test: the drill_info-local ``UserSchema`` used to declare an ``email`` field. ``DatasetDrillInfoSchema`` nests it (unfiltered by ``select_columns``) for both ``created_by`` and ``changed_by``, so any user with dataset-read access -- or, via the dashboard fallback, embedded guests -- received maintainer email addresses. Dataset-read access does not imply entitlement to other users' PII. """ from superset.datasets.schemas import UserSchema class _FakeUser: first_name = "Jane" last_name = "Doe" email = "jane.doe@example.com" dumped = UserSchema().dump(_FakeUser()) assert "email" not in dumped assert dumped == {"first_name": "Jane", "last_name": "Doe"} def test_drill_info_editor_schema_does_not_expose_secondary_label() -> None: """ Regression test: ``DatasetDrillInfoSchema.editors`` used to nest the shared ``SubjectResponseSchema``, which includes ``secondary_label``. For a user-backed Subject, user-subject synchronization (``superset.subjects.sync.sync_user_subject``) stores that user's email in ``secondary_label``, so nesting it unfiltered leaked the same maintainer PII that dropping ``email`` from ``UserSchema`` was meant to close, just under a different field name. """ from superset.datasets.schemas import DrillInfoEditorSchema class _FakeEditorSubject: id = 1 label = "Jane Doe" secondary_label = "jane.doe@example.com" img = "avatar.png" type = 1 dumped = DrillInfoEditorSchema().dump(_FakeEditorSubject()) assert "secondary_label" not in dumped assert dumped == {"id": 1, "label": "Jane Doe", "img": "avatar.png", "type": 1} def test_dataset_post_schema_has_all_put_scalar_fields() -> None: """ Every scalar model field accepted by DatasetPutSchema should also be accepted by DatasetPostSchema, unless it is intentionally excluded (fields that only make sense after a dataset already exists). This prevents the class of bug where a new column is added to the update schema but forgotten in the create schema. """ from superset.datasets.schemas import DatasetPostSchema, DatasetPutSchema # Fields that are intentionally only on Put: they require an existing dataset # or are populated server-side during creation. put_only_fields = { "columns", "metrics", "folders", "database_id", # Post uses "database" (integer id) instead "description", "main_dttm_col", "filter_select_enabled", "fetch_values_predicate", "offset", "default_endpoint", "cache_timeout", "is_sqllab_view", "extra", } put_fields = set(DatasetPutSchema().fields.keys()) post_fields = set(DatasetPostSchema().fields.keys()) missing = put_fields - post_fields - put_only_fields assert missing == set(), ( f"Fields {missing} are in DatasetPutSchema but missing from " f"DatasetPostSchema. Either add them to DatasetPostSchema or to " f"the put_only_fields exclusion list in this test." ) def test_dataset_post_schema_includes_currency_code_column() -> None: """Test that DatasetPostSchema accepts currency_code_column.""" from superset.datasets.schemas import DatasetPostSchema schema = DatasetPostSchema() data = { "database": 1, "table_name": "virtual_dataset", "currency_code_column": "currency", } result = schema.load(data) assert result["currency_code_column"] == "currency" def test_dataset_metrics_put_schema_parses_currency_string() -> None: """Test that DatasetMetricsPutSchema parses string currency payloads.""" from superset.datasets.schemas import DatasetMetricsPutSchema schema = DatasetMetricsPutSchema() data = { "expression": "SUM(amount)", "metric_name": "sum_amount", "currency": '{"symbol": "EUR", "symbolPosition": "suffix"}', } result = schema.load(data) assert result["currency"] == {"symbol": "EUR", "symbolPosition": "suffix"} def test_dataset_metrics_put_schema_parses_python_dict_string() -> None: """Test that DatasetMetricsPutSchema parses Python dict currency strings.""" from superset.datasets.schemas import DatasetMetricsPutSchema schema = DatasetMetricsPutSchema() data = { "expression": "SUM(amount)", "metric_name": "sum_amount", "currency": "{'symbol': 'GBP', 'symbolPosition': 'prefix'}", } result = schema.load(data) assert result["currency"] == {"symbol": "GBP", "symbolPosition": "prefix"} def test_dataset_metrics_put_schema_handles_malformed_currency() -> None: """Test that DatasetMetricsPutSchema normalizes malformed currency strings.""" from superset.datasets.schemas import DatasetMetricsPutSchema schema = DatasetMetricsPutSchema() data = { "expression": "SUM(amount)", "metric_name": "sum_amount", "currency": "not valid json", } result = schema.load(data) assert result["currency"] == {} def test_import_v1_metric_schema_parses_currency_string() -> None: """Test that ImportV1MetricSchema parses string currency payloads.""" from superset.datasets.schemas import ImportV1MetricSchema schema = ImportV1MetricSchema() data = { "metric_name": "sum_amount", "expression": "SUM(amount)", "currency": '{"symbol": "CAD", "symbolPosition": "suffix"}', } result = schema.load(data) assert result["currency"] == {"symbol": "CAD", "symbolPosition": "suffix"}