# 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. from superset import db from superset.models.core import Theme from superset.utils import json from tests.integration_tests.base_tests import SupersetTestCase class TestThemeApiEndpoints(SupersetTestCase): """Integration tests for Theme API endpoints""" def setUp(self): """Set up test fixtures""" super().setUp() self.login("admin") # Clean up any existing themes db.session.query(Theme).delete() db.session.commit() def tearDown(self): """Clean up after tests""" # Clean up any themes created during tests db.session.query(Theme).delete() db.session.commit() super().tearDown() def test_post_theme_success(self): """Test successful theme creation via POST""" # Arrange theme_data = { "theme_name": "Test Theme", "json_data": json.dumps({"algorithm": "default", "token": {}}), } # Act response = self.client.post( "/api/v1/theme/", json=theme_data, headers={"Content-Type": "application/json"}, ) # Assert assert response.status_code == 201 response_data = json.loads(response.data) assert "id" in response_data # Verify theme was created in database created_theme = ( db.session.query(Theme).filter_by(theme_name="Test Theme").first() ) assert created_theme is not None assert created_theme.is_system is False assert created_theme.uuid is not None def test_post_theme_validation_error(self): """Test POST with invalid data returns validation error""" # Arrange invalid_data = { "theme_name": "", # Required field empty "json_data": json.dumps({"algorithm": "default"}), } # Act response = self.client.post( "/api/v1/theme/", json=invalid_data, headers={"Content-Type": "application/json"}, ) # Assert assert response.status_code == 400 def test_post_theme_missing_required_field(self): """Test POST with missing required field""" # Arrange incomplete_data = { "theme_name": "Test Theme" # Missing json_data } # Act response = self.client.post( "/api/v1/theme/", json=incomplete_data, headers={"Content-Type": "application/json"}, ) # Assert assert response.status_code == 400 def test_put_theme_success(self): """Test successful theme update via PUT""" # Arrange - Create a theme first theme = Theme( theme_name="Original Theme", json_data=json.dumps({"algorithm": "default"}), is_system=False, ) db.session.add(theme) db.session.commit() update_data = { "theme_name": "Updated Theme", "json_data": json.dumps({"algorithm": "dark"}), } # Act response = self.client.put( f"/api/v1/theme/{theme.id}", json=update_data, headers={"Content-Type": "application/json"}, ) # Assert assert response.status_code == 200 # Verify theme was updated updated_theme = db.session.query(Theme).get(theme.id) assert updated_theme.theme_name == "Updated Theme" assert '"algorithm": "dark"' in updated_theme.json_data def test_put_theme_filters_readonly_fields(self): """Test PUT filters out read-only fields like is_system and uuid""" # Arrange - Create a theme first theme = Theme( theme_name="Original Theme", json_data=json.dumps({"algorithm": "default"}), is_system=False, ) db.session.add(theme) db.session.commit() original_uuid = theme.uuid # Try to update with read-only fields update_data = { "theme_name": "Updated Theme", "json_data": json.dumps({"algorithm": "dark"}), "is_system": True, # Should be filtered out "uuid": "new-uuid", # Should be filtered out } # Act response = self.client.put( f"/api/v1/theme/{theme.id}", json=update_data, headers={"Content-Type": "application/json"}, ) # Assert assert response.status_code == 200 # Verify only editable fields were updated updated_theme = db.session.query(Theme).get(theme.id) assert updated_theme.theme_name == "Updated Theme" assert updated_theme.is_system is False # Should remain unchanged assert updated_theme.uuid == original_uuid # Should remain unchanged def test_put_system_theme_protection(self): """Test PUT fails when trying to update system theme""" # Arrange - Create a system theme system_theme = Theme( theme_name="System Theme", json_data=json.dumps({"algorithm": "default"}), is_system=True, ) db.session.add(system_theme) db.session.commit() update_data = { "theme_name": "Hacked System Theme", "json_data": json.dumps({"algorithm": "dark"}), } # Act response = self.client.put( f"/api/v1/theme/{system_theme.id}", json=update_data, headers={"Content-Type": "application/json"}, ) # Assert assert response.status_code == 403 response_data = json.loads(response.data) assert "Forbidden" in response_data.get("message", "") def test_put_theme_not_found(self): """Test PUT with non-existent theme ID""" # Arrange update_data = { "theme_name": "Updated Theme", "json_data": json.dumps({"algorithm": "dark"}), } # Act response = self.client.put( "/api/v1/theme/99999", # Non-existent ID json=update_data, headers={"Content-Type": "application/json"}, ) # Assert assert response.status_code == 404 def test_bulk_delete_excludes_system_themes(self): """Test bulk delete excludes system themes""" # Arrange - Create regular and system themes regular_theme = Theme( theme_name="Regular Theme", json_data=json.dumps({"algorithm": "default"}), is_system=False, ) system_theme = Theme( theme_name="System Theme", json_data=json.dumps({"algorithm": "default"}), is_system=True, ) db.session.add_all([regular_theme, system_theme]) db.session.commit() # Act - Try to delete both themes response = self.client.delete( f"/api/v1/theme/?q={json.dumps([regular_theme.id, system_theme.id])}" ) # Assert assert response.status_code == 403 # Should fail due to system theme protection # Verify both themes still exist assert db.session.query(Theme).get(regular_theme.id) is not None assert db.session.query(Theme).get(system_theme.id) is not None def test_bulk_delete_regular_themes_only(self): """Test bulk delete works with regular themes only""" # Arrange - Create only regular themes theme1 = Theme( theme_name="Theme 1", json_data=json.dumps({"algorithm": "default"}), is_system=False, ) theme2 = Theme( theme_name="Theme 2", json_data=json.dumps({"algorithm": "default"}), is_system=False, ) db.session.add_all([theme1, theme2]) db.session.commit() # Act response = self.client.delete( f"/api/v1/theme/?q={json.dumps([theme1.id, theme2.id])}" ) # Assert assert response.status_code == 200 # Verify themes were deleted assert db.session.query(Theme).get(theme1.id) is None assert db.session.query(Theme).get(theme2.id) is None def test_get_theme_includes_new_fields(self): """Test GET theme includes is_system and uuid fields""" # Arrange theme = Theme( theme_name="Test Theme", json_data=json.dumps({"algorithm": "default"}), is_system=False, ) db.session.add(theme) db.session.commit() # Act response = self.client.get(f"/api/v1/theme/{theme.id}") # Assert assert response.status_code == 200 response_data = json.loads(response.data) result = response_data.get("result", {}) assert "is_system" in result assert "uuid" in result assert result["is_system"] is False assert result["uuid"] is not None def test_get_theme_list_includes_new_fields(self): """Test GET theme list includes is_system and uuid fields""" # Arrange theme = Theme( theme_name="Test Theme", json_data=json.dumps({"algorithm": "default"}), is_system=True, ) db.session.add(theme) db.session.commit() # Act response = self.client.get("/api/v1/theme/") # Assert assert response.status_code == 200 response_data = json.loads(response.data) results = response_data.get("result", []) assert len(results) > 0 theme_data = results[0] assert "is_system" in theme_data assert "uuid" in theme_data