Files
superset2/tests/integration_tests/themes/api_endpoints_test.py
Maxime Beauchemin e741a3167f feat: add a theme CRUD page to manage themes (#34182)
Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
2025-07-25 13:26:41 -07:00

323 lines
10 KiB
Python

# 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