Files
superset2/tests/unit_tests/views/test_error_handling.py
T

183 lines
6.9 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.
import logging
from typing import cast
from unittest.mock import patch
import pytest
import sshtunnel
from flask import Flask, Response
from flask_babel import Babel
from superset.errors import SupersetErrorType
from superset.exceptions import QueryObjectValidationError, SupersetException
from superset.superset_typing import FlaskResponse
from superset.utils import json
from superset.views.error_handling import handle_api_exception, set_app_error_handlers
class TestHandleApiExceptionSSHTunnelError:
def test_returns_400_with_connection_host_down_error_and_no_error_log(
self, app, caplog: pytest.LogCaptureFixture
):
@handle_api_exception
def view(self: object) -> FlaskResponse:
raise sshtunnel.BaseSSHTunnelForwarderError(
"Could not establish session to SSH gateway"
)
with app.test_request_context():
with caplog.at_level(logging.WARNING):
response = cast(Response, view(self=object()))
assert response.status_code == 400
payload = json.loads(response.data)
assert (
payload["errors"][0]["error_type"]
== SupersetErrorType.CONNECTION_HOST_DOWN_ERROR.value
)
assert not any(record.levelno >= logging.ERROR for record in caplog.records)
assert any(
record.levelno == logging.WARNING
and "BaseSSHTunnelForwarderError" in record.message
for record in caplog.records
)
class TestShowUnexpectedException:
def _build_app_with_handlers(self) -> Flask:
# A fresh, minimal Flask app per test: `set_app_error_handlers` can
# only register handlers before the app has served its first
# request, so it can't share the module-scoped `app` fixture across
# tests in this class.
test_app = Flask(__name__)
test_app.config["DEBUG"] = False
Babel(test_app)
set_app_error_handlers(test_app)
@test_app.route("/ssh-tunnel-error")
def ssh_tunnel_error_view() -> FlaskResponse:
raise sshtunnel.BaseSSHTunnelForwarderError(
"Could not establish session to SSH gateway"
)
@test_app.route("/generic-error")
def generic_error_view() -> FlaskResponse:
raise ValueError("boom")
return test_app
def test_ssh_tunnel_error_returns_structured_400(
self, caplog: pytest.LogCaptureFixture
):
client = self._build_app_with_handlers().test_client()
with caplog.at_level(logging.WARNING):
response = client.get("/ssh-tunnel-error")
assert response.status_code == 400
payload = json.loads(response.data)
assert (
payload["errors"][0]["error_type"]
== SupersetErrorType.CONNECTION_HOST_DOWN_ERROR.value
)
assert not any(record.levelno >= logging.ERROR for record in caplog.records)
def test_generic_exception_still_returns_original_500_shape(
self, caplog: pytest.LogCaptureFixture
):
client = self._build_app_with_handlers().test_client()
with caplog.at_level(logging.WARNING):
response = client.get("/generic-error")
assert response.status_code == 500
payload = json.loads(response.data)
assert (
payload["errors"][0]["error_type"]
== SupersetErrorType.GENERIC_BACKEND_ERROR.value
)
assert any(record.levelno >= logging.ERROR for record in caplog.records)
class TestShowSupersetException:
def _build_app_with_handlers(self) -> Flask:
# A fresh, minimal Flask app per test: `set_app_error_handlers` can
# only register handlers before the app has served its first
# request, so it can't share the module-scoped `app` fixture across
# tests in this class.
test_app = Flask(__name__)
test_app.config["DEBUG"] = False
Babel(test_app)
set_app_error_handlers(test_app)
@test_app.route("/query-validation-error")
def query_validation_error_view() -> FlaskResponse:
raise QueryObjectValidationError("list object has no element 0")
@test_app.route("/generic-superset-exception")
def generic_superset_exception_view() -> FlaskResponse:
raise SupersetException("boom")
return test_app
def test_4xx_superset_exception_returns_its_status_and_logs_at_warning(
self, caplog: pytest.LogCaptureFixture
):
client = self._build_app_with_handlers().test_client()
with caplog.at_level(logging.WARNING):
response = client.get("/query-validation-error")
assert response.status_code == 400
payload = json.loads(response.data)
assert payload["errors"][0]["message"] == "list object has no element 0"
assert not any(record.levelno >= logging.ERROR for record in caplog.records)
assert any(
record.levelno == logging.WARNING
and record.message == "list object has no element 0"
for record in caplog.records
)
def test_5xx_superset_exception_still_returns_500_and_logs_at_error(
self, caplog: pytest.LogCaptureFixture
):
client = self._build_app_with_handlers().test_client()
with caplog.at_level(logging.WARNING):
response = client.get("/generic-superset-exception")
assert response.status_code == 500
payload = json.loads(response.data)
assert payload["errors"][0]["message"] == "boom"
assert any(record.levelno >= logging.ERROR for record in caplog.records)
def test_html_accept_serves_branded_error_page_not_raw_json(self):
client = self._build_app_with_handlers().test_client()
with patch(
"superset.views.error_handling.send_file",
return_value=Response("<html>500</html>", mimetype="text/html"),
) as mock_send_file:
response = client.get(
"/generic-superset-exception", headers={"Accept": "text/html"}
)
assert response.status_code == 500
assert response.content_type.startswith("text/html")
mock_send_file.assert_called_once()