feat: add extract_errors to Postgres (#13997)

* feat: add extract_errors to Postgres

* Add unit tests

* Fix lint

* Fix unit tests
This commit is contained in:
Beto Dealmeida 2021-04-08 13:24:54 -07:00 committed by GitHub
parent 784d29b57c
commit c60a93db9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 248 additions and 228 deletions

View File

@ -131,3 +131,12 @@ Superset encountered an unexpected error.
Someething unexpected happened in the Superset backend. Please reach out
to your administrator.
## Issue 1012
```
The username provided when connecting to a database is not valid.
```
The user provided a username that doesn't exist in the database. Please check
that the username is typed correctly and exists in the database.

View File

@ -28,6 +28,10 @@ export const ErrorTypeEnum = {
GENERIC_DB_ENGINE_ERROR: 'GENERIC_DB_ENGINE_ERROR',
COLUMN_DOES_NOT_EXIST_ERROR: 'COLUMN_DOES_NOT_EXIST_ERROR',
TABLE_DOES_NOT_EXIST_ERROR: 'TABLE_DOES_NOT_EXIST_ERROR',
TEST_CONNECTION_INVALID_USERNAME_ERROR:
'TEST_CONNECTION_INVALID_USERNAME_ERROR',
TEST_CONNECTION_INVALID_HOSTNAME_ERROR:
'TEST_CONNECTION_INVALID_HOSTNAME_ERROR',
TEST_CONNECTION_PORT_CLOSED_ERROR: 'TEST_CONNECTION_PORT_CLOSED_ERROR',
TEST_CONNECTION_HOST_DOWN_ERROR: 'TEST_CONNECTION_HOST_DOWN_ERROR',
@ -48,8 +52,6 @@ export const ErrorTypeEnum = {
// Sqllab error
MISSING_TEMPLATE_PARAMS_ERROR: 'MISSING_TEMPLATE_PARAMS_ERROR',
TEST_CONNECTION_INVALID_HOSTNAME_ERROR:
'TEST_CONNECTION_INVALID_HOSTNAME_ERROR',
// Generic errors
GENERIC_COMMAND_ERROR: 'GENERIC_COMMAND_ERROR',

View File

@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import dataclasses
import json
import logging
import re
@ -1456,7 +1457,9 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
"Query %s on schema %s failed", sql, self.schema, exc_info=True
)
db_engine_spec = self.database.db_engine_spec
errors = db_engine_spec.extract_errors(ex)
errors = [
dataclasses.asdict(error) for error in db_engine_spec.extract_errors(ex)
]
error_message = utils.error_msg_from_exception(ex)
return QueryResult(

View File

@ -25,7 +25,7 @@ from superset.commands.exceptions import (
ImportFailedError,
UpdateFailedError,
)
from superset.exceptions import SupersetErrorException
from superset.exceptions import SupersetErrorsException
class DatabaseInvalidError(CommandInvalidError):
@ -117,26 +117,22 @@ class DatabaseDeleteFailedReportsExistError(DatabaseDeleteFailedError):
message = _("There are associated alerts or reports")
class DatabaseTestConnectionFailedError(CommandException):
class DatabaseTestConnectionFailedError(SupersetErrorsException):
status = 422
message = _("Connection failed, please check your connection settings")
class DatabaseSecurityUnsafeError(DatabaseTestConnectionFailedError):
class DatabaseSecurityUnsafeError(CommandInvalidError):
message = _("Stopped an unsafe database connection")
class DatabaseTestConnectionDriverError(DatabaseTestConnectionFailedError):
class DatabaseTestConnectionDriverError(CommandInvalidError):
message = _("Could not load database driver")
class DatabaseTestConnectionUnexpectedError(DatabaseTestConnectionFailedError):
class DatabaseTestConnectionUnexpectedError(CommandInvalidError):
message = _("Unexpected error occurred, please check your logs for details")
class DatabaseImportError(ImportFailedError):
message = _("Import database failed for an unknown reason")
class DatabaseTestConnectionNetworkError(SupersetErrorException):
status = 400

View File

@ -20,7 +20,6 @@ from typing import Any, Dict, Optional
from flask_appbuilder.security.sqla.models import User
from flask_babel import gettext as _
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import DBAPIError, NoSuchModuleError
from superset.commands.base import BaseCommand
@ -28,15 +27,12 @@ from superset.databases.commands.exceptions import (
DatabaseSecurityUnsafeError,
DatabaseTestConnectionDriverError,
DatabaseTestConnectionFailedError,
DatabaseTestConnectionNetworkError,
DatabaseTestConnectionUnexpectedError,
)
from superset.databases.dao import DatabaseDAO
from superset.errors import ErrorLevel, SupersetErrorType
from superset.exceptions import SupersetSecurityException
from superset.extensions import event_logger
from superset.models.core import Database
from superset.utils.network import is_host_up, is_hostname_valid, is_port_open
logger = logging.getLogger(__name__)
@ -47,53 +43,6 @@ class TestConnectionDatabaseCommand(BaseCommand):
self._properties = data.copy()
self._model: Optional[Database] = None
@staticmethod
def _diagnose(uri: str) -> None:
parsed_uri = make_url(uri)
if parsed_uri.host:
if not is_hostname_valid(parsed_uri.host):
raise DatabaseTestConnectionNetworkError(
error_type=SupersetErrorType.TEST_CONNECTION_INVALID_HOSTNAME_ERROR,
message=_(
'Unable to resolve hostname "%(hostname)s".',
hostname=parsed_uri.host,
),
level=ErrorLevel.ERROR,
extra={"hostname": parsed_uri.host},
)
if parsed_uri.port:
if not is_port_open(parsed_uri.host, parsed_uri.port):
if is_host_up(parsed_uri.host):
raise DatabaseTestConnectionNetworkError(
error_type=(
SupersetErrorType.TEST_CONNECTION_PORT_CLOSED_ERROR
),
message=_(
"The host %(host)s is up, but the port %(port)s is "
"closed.",
host=parsed_uri.host,
port=parsed_uri.port,
),
level=ErrorLevel.ERROR,
extra={
"hostname": parsed_uri.host,
"port": parsed_uri.port,
},
)
raise DatabaseTestConnectionNetworkError(
error_type=SupersetErrorType.TEST_CONNECTION_HOST_DOWN_ERROR,
message=_(
"The host %(host)s might be down, ond can't be reached on "
"port %(port)s.",
host=parsed_uri.host,
port=parsed_uri.port,
),
level=ErrorLevel.ERROR,
extra={"hostname": parsed_uri.host, "port": parsed_uri.port,},
)
def run(self) -> None:
self.validate()
uri = self._properties.get("sqlalchemy_uri", "")
@ -136,9 +85,9 @@ class TestConnectionDatabaseCommand(BaseCommand):
action=f"test_connection_error.{ex.__class__.__name__}",
engine=database.db_engine_spec.__name__,
)
# check if we have connectivity to the host, and if the port is open
self._diagnose(uri)
raise DatabaseTestConnectionFailedError()
# check for custom errors (wrong username, wrong password, etc)
errors = database.db_engine_spec.extract_errors(ex)
raise DatabaseTestConnectionFailedError(errors)
except SupersetSecurityException as ex:
event_logger.log_with_context(
action=f"test_connection_error.{ex.__class__.__name__}",

View File

@ -15,7 +15,6 @@
# specific language governing permissions and limitations
# under the License.
# pylint: disable=unused-argument
import dataclasses
import hashlib
import json
import logging
@ -266,6 +265,7 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
max_column_name_length = 0
try_remove_schema_from_table_name = True # pylint: disable=invalid-name
run_multiple_statements_as_one = False
custom_errors: Dict[Pattern[str], Tuple[str, SupersetErrorType]] = {}
@classmethod
def get_dbapi_exception_mapping(cls) -> Dict[Type[Exception], Type[Exception]]:
@ -746,15 +746,27 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
return utils.error_msg_from_exception(ex)
@classmethod
def extract_errors(cls, ex: Exception) -> List[Dict[str, Any]]:
def extract_errors(cls, ex: Exception) -> List[SupersetError]:
raw_message = cls._extract_error_message(ex)
for regex, (message, error_type) in cls.custom_errors.items():
match = regex.search(raw_message)
if match:
return [
SupersetError(
error_type=error_type,
message=message % match.groupdict(),
level=ErrorLevel.ERROR,
extra={"engine_name": cls.engine_name},
)
]
return [
dataclasses.asdict(
SupersetError(
error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
message=cls._extract_error_message(ex),
level=ErrorLevel.ERROR,
extra={"engine_name": cls.engine_name},
)
SupersetError(
error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
message=cls._extract_error_message(ex),
level=ErrorLevel.ERROR,
extra={"engine_name": cls.engine_name},
)
]

View File

@ -31,12 +31,14 @@ from typing import (
Union,
)
from flask_babel import gettext as __
from pytz import _FixedOffset # type: ignore
from sqlalchemy.dialects.postgresql import ARRAY, DOUBLE_PRECISION, ENUM, JSON
from sqlalchemy.dialects.postgresql.base import PGInspector
from sqlalchemy.types import String, TypeEngine
from superset.db_engine_specs.base import BaseEngineSpec
from superset.errors import SupersetErrorType
from superset.exceptions import SupersetException
from superset.utils import core as utils
from superset.utils.core import ColumnSpec, GenericDataType
@ -53,6 +55,24 @@ class FixedOffsetTimezone(_FixedOffset):
pass
# Regular expressions to catch custom errors
INVALID_USERNAME_REGEX = re.compile('role "(?P<username>.*?)" does not exist')
INVALID_HOSTNAME_REGEX = re.compile(
'could not translate host name "(?P<hostname>.*?)" to address: '
"nodename nor servname provided, or not known"
)
CONNECTION_PORT_CLOSED_REGEX = re.compile(
r"could not connect to server: Connection refused\s+Is the server "
r'running on host "(?P<hostname>.*?)" (\(.*?\) )?and accepting\s+TCP/IP '
r"connections on port (?P<port>.*?)\?"
)
CONNECTION_HOST_DOWN_REGEX = re.compile(
r"could not connect to server: (?P<reason>.*?)\s+Is the server running on "
r'host "(?P<hostname>.*?)" (\(.*?\) )?and accepting\s+TCP/IP '
r"connections on port (?P<port>.*?)\?"
)
class PostgresBaseEngineSpec(BaseEngineSpec):
""" Abstract class for Postgres 'like' databases """
@ -71,6 +91,28 @@ class PostgresBaseEngineSpec(BaseEngineSpec):
"P1Y": "DATE_TRUNC('year', {col})",
}
custom_errors = {
INVALID_USERNAME_REGEX: (
__('The username "%(username)s" does not exist.'),
SupersetErrorType.TEST_CONNECTION_INVALID_USERNAME_ERROR,
),
INVALID_HOSTNAME_REGEX: (
__('The hostname "%(hostname)s" cannot be resolved.'),
SupersetErrorType.TEST_CONNECTION_INVALID_HOSTNAME_ERROR,
),
CONNECTION_PORT_CLOSED_REGEX: (
__("Port %(port)s on hostname %(hostname)s refused the connection."),
SupersetErrorType.TEST_CONNECTION_PORT_CLOSED_ERROR,
),
CONNECTION_HOST_DOWN_REGEX: (
__(
"The host %(hostname)s might be down, and can't be "
"reached on port %(port)s"
),
SupersetErrorType.TEST_CONNECTION_HOST_DOWN_ERROR,
),
}
@classmethod
def fetch_data(
cls, cursor: Any, limit: Optional[int] = None

View File

@ -14,7 +14,6 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import dataclasses
import logging
import re
import textwrap
@ -1133,54 +1132,41 @@ class PrestoEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-metho
return database.get_df("SHOW FUNCTIONS")["Function"].tolist()
@classmethod
def extract_errors(cls, ex: Exception) -> List[Dict[str, Any]]:
def extract_errors(cls, ex: Exception) -> List[SupersetError]:
raw_message = cls._extract_error_message(ex)
column_match = re.search(COLUMN_NOT_RESOLVED_ERROR_REGEX, raw_message)
if column_match:
return [
dataclasses.asdict(
SupersetError(
error_type=SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR,
message=__(
'We can\'t seem to resolve the column "%(column_name)s" at '
"line %(location)s.",
column_name=column_match.group(2),
location=column_match.group(1),
),
level=ErrorLevel.ERROR,
extra={"engine_name": cls.engine_name},
)
SupersetError(
error_type=SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR,
message=__(
'We can\'t seem to resolve the column "%(column_name)s" at '
"line %(location)s.",
column_name=column_match.group(2),
location=column_match.group(1),
),
level=ErrorLevel.ERROR,
extra={"engine_name": cls.engine_name},
)
]
table_match = re.search(TABLE_DOES_NOT_EXIST_ERROR_REGEX, raw_message)
if table_match:
return [
dataclasses.asdict(
SupersetError(
error_type=SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
message=__(
'The table "%(table_name)s" does not exist. '
"A valid table must be used to run this query.",
table_name=table_match.group(1),
),
level=ErrorLevel.ERROR,
extra={"engine_name": cls.engine_name},
)
)
]
return [
dataclasses.asdict(
SupersetError(
error_type=SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
message=cls._extract_error_message(ex),
error_type=SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR,
message=__(
'The table "%(table_name)s" does not exist. '
"A valid table must be used to run this query.",
table_name=table_match.group(1),
),
level=ErrorLevel.ERROR,
extra={"engine_name": cls.engine_name},
)
)
]
]
return super().extract_errors(ex)
@classmethod
def is_readonly_query(cls, parsed_query: ParsedQuery) -> bool:

View File

@ -39,6 +39,8 @@ class SupersetErrorType(str, Enum):
GENERIC_DB_ENGINE_ERROR = "GENERIC_DB_ENGINE_ERROR"
COLUMN_DOES_NOT_EXIST_ERROR = "COLUMN_DOES_NOT_EXIST_ERROR"
TABLE_DOES_NOT_EXIST_ERROR = "TABLE_DOES_NOT_EXIST_ERROR"
TEST_CONNECTION_INVALID_USERNAME_ERROR = "TEST_CONNECTION_INVALID_USERNAME_ERROR"
TEST_CONNECTION_INVALID_HOSTNAME_ERROR = "TEST_CONNECTION_INVALID_HOSTNAME_ERROR"
TEST_CONNECTION_PORT_CLOSED_ERROR = "TEST_CONNECTION_PORT_CLOSED_ERROR"
TEST_CONNECTION_HOST_DOWN_ERROR = "TEST_CONNECTION_HOST_DOWN_ERROR"
@ -58,7 +60,6 @@ class SupersetErrorType(str, Enum):
# Sql Lab errors
MISSING_TEMPLATE_PARAMS_ERROR = "MISSING_TEMPLATE_PARAMS_ERROR"
TEST_CONNECTION_INVALID_HOSTNAME_ERROR = "TEST_CONNECTION_INVALID_HOSTNAME_ERROR"
# Generic errors
GENERIC_COMMAND_ERROR = "GENERIC_COMMAND_ERROR"
@ -153,6 +154,15 @@ ERROR_TYPES_TO_ISSUE_CODES_MAPPING = {
"message": _("Issue 1011 - Superset encountered an unexpected error."),
},
],
SupersetErrorType.TEST_CONNECTION_INVALID_USERNAME_ERROR: [
{
"code": 1012,
"message": _(
"Issue 1012 - The username provided when "
"connecting to a database is not valid."
),
},
],
}

View File

@ -17,6 +17,7 @@
# isort:skip_file
# pylint: disable=invalid-name, no-self-use, too-many-public-methods, too-many-arguments
"""Unit tests for Superset"""
import dataclasses
import json
from io import BytesIO
from unittest import mock
@ -27,10 +28,12 @@ import pytest
import yaml
from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import DBAPIError
from sqlalchemy.sql import func
from superset import db, security_manager
from superset.connectors.sqla.models import SqlaTable
from superset.errors import SupersetError
from superset.models.core import Database
from superset.models.reports import ReportSchedule, ReportScheduleType
from superset.utils.core import get_example_database, get_main_database
@ -895,16 +898,44 @@ class TestDatabaseApi(SupersetTestCase):
app.config["PREVENT_UNSAFE_DB_CONNECTIONS"] = False
@mock.patch("superset.databases.commands.test_connection.is_hostname_valid",)
def test_test_connection_failed_invalid_hostname(self, mock_is_hostname_valid):
@mock.patch(
"superset.databases.commands.test_connection.DatabaseDAO.build_db_for_connection_test",
)
@mock.patch("superset.databases.commands.test_connection.event_logger",)
def test_test_connection_failed_invalid_hostname(
self, mock_event_logger, mock_build_db
):
"""
Database API: Test test connection failed due to invalid hostname
"""
mock_is_hostname_valid.return_value = False
msg = 'psql: error: could not translate host name "locahost" to address: nodename nor servname provided, or not known'
mock_build_db.return_value.set_sqlalchemy_uri.side_effect = DBAPIError(
msg, None, None
)
mock_build_db.return_value.db_engine_spec.__name__ = "Some name"
superset_error = SupersetError(
message='Unable to resolve hostname "locahost".',
error_type="TEST_CONNECTION_INVALID_HOSTNAME_ERROR",
level="error",
extra={
"hostname": "locahost",
"issue_codes": [
{
"code": 1007,
"message": (
"Issue 1007 - The hostname provided can't be resolved."
),
}
],
},
)
mock_build_db.return_value.db_engine_spec.extract_errors.return_value = [
superset_error
]
self.login("admin")
data = {
"sqlalchemy_uri": "postgres://username:password@invalidhostname:12345/db",
"sqlalchemy_uri": "postgres://username:password@locahost:12345/db",
"database_name": "examples",
"impersonate_user": False,
"server_cert": None,
@ -912,121 +943,10 @@ class TestDatabaseApi(SupersetTestCase):
url = "api/v1/database/test_connection"
rv = self.post_assert_metric(url, data, "test_connection")
assert rv.status_code == 400
assert rv.status_code == 422
assert rv.headers["Content-Type"] == "application/json; charset=utf-8"
response = json.loads(rv.data.decode("utf-8"))
expected_response = {
"errors": [
{
"message": 'Unable to resolve hostname "invalidhostname".',
"error_type": "TEST_CONNECTION_INVALID_HOSTNAME_ERROR",
"level": "error",
"extra": {
"hostname": "invalidhostname",
"issue_codes": [
{
"code": 1007,
"message": "Issue 1007 - The hostname provided can't be resolved.",
}
],
},
}
]
}
assert response == expected_response
@mock.patch("superset.databases.commands.test_connection.is_hostname_valid")
@mock.patch("superset.databases.commands.test_connection.is_port_open")
@mock.patch("superset.databases.commands.test_connection.is_host_up")
def test_test_connection_failed_closed_port(
self, mock_is_host_up, mock_is_port_open, mock_is_hostname_valid
):
"""
Database API: Test test connection failed due to closed port.
"""
mock_is_hostname_valid.return_value = True
mock_is_port_open.return_value = False
mock_is_host_up.return_value = True
self.login("admin")
data = {
"sqlalchemy_uri": "postgres://username:password@localhost:12345/db",
"database_name": "examples",
"impersonate_user": False,
"server_cert": None,
}
url = "api/v1/database/test_connection"
rv = self.post_assert_metric(url, data, "test_connection")
assert rv.status_code == 400
assert rv.headers["Content-Type"] == "application/json; charset=utf-8"
response = json.loads(rv.data.decode("utf-8"))
expected_response = {
"errors": [
{
"message": "The host localhost is up, but the port 12345 is closed.",
"error_type": "TEST_CONNECTION_PORT_CLOSED_ERROR",
"level": "error",
"extra": {
"hostname": "localhost",
"port": 12345,
"issue_codes": [
{
"code": 1008,
"message": "Issue 1008 - The port is closed.",
}
],
},
}
]
}
assert response == expected_response
@mock.patch("superset.databases.commands.test_connection.is_hostname_valid")
@mock.patch("superset.databases.commands.test_connection.is_port_open")
@mock.patch("superset.databases.commands.test_connection.is_host_up")
def test_test_connection_failed_host_down(
self, mock_is_host_up, mock_is_port_open, mock_is_hostname_valid
):
"""
Database API: Test test connection failed due to host being down.
"""
mock_is_hostname_valid.return_value = True
mock_is_port_open.return_value = False
mock_is_host_up.return_value = False
self.login("admin")
data = {
"sqlalchemy_uri": "postgres://username:password@localhost:12345/db",
"database_name": "examples",
"impersonate_user": False,
"server_cert": None,
}
url = "api/v1/database/test_connection"
rv = self.post_assert_metric(url, data, "test_connection")
assert rv.status_code == 400
assert rv.headers["Content-Type"] == "application/json; charset=utf-8"
response = json.loads(rv.data.decode("utf-8"))
expected_response = {
"errors": [
{
"message": "The host localhost might be down, ond can't be reached on port 12345.",
"error_type": "TEST_CONNECTION_HOST_DOWN_ERROR",
"level": "error",
"extra": {
"hostname": "localhost",
"port": 12345,
"issue_codes": [
{
"code": 1009,
"message": "Issue 1009 - The host might be down, and can't be reached on the provided port.",
}
],
},
}
]
}
expected_response = {"errors": [dataclasses.asdict(superset_error)]}
assert response == expected_response
@pytest.mark.usefixtures(

View File

@ -14,6 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from textwrap import dedent
from unittest import mock
from sqlalchemy import column, literal_column
@ -21,6 +22,7 @@ from sqlalchemy.dialects import postgresql
from superset.db_engine_specs import engines
from superset.db_engine_specs.postgres import PostgresEngineSpec
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
from tests.db_engine_specs.base_tests import TestDbEngineSpec
from tests.fixtures.certificates import ssl_certificate
from tests.fixtures.database import default_db_extra
@ -171,7 +173,9 @@ class TestPostgresDbEngineSpec(TestDbEngineSpec):
)
sql = "SELECT * FROM birth_names"
results = PostgresEngineSpec.estimate_statement_cost(sql, cursor)
self.assertEqual(results, {"Start-up cost": 0.00, "Total cost": 1537.91,})
self.assertEqual(
results, {"Start-up cost": 0.00, "Total cost": 1537.91,},
)
def test_estimate_statement_invalid_syntax(self):
"""
@ -207,3 +211,90 @@ class TestPostgresDbEngineSpec(TestDbEngineSpec):
{"Start-up cost": "10.0", "Total cost": "1537.0",},
],
)
def test_extract_errors(self):
"""
Test that custom error messages are extracted correctly.
"""
msg = 'psql: error: FATAL: role "testuser" does not exist'
result = PostgresEngineSpec.extract_errors(Exception(msg))
assert result == [
SupersetError(
error_type=SupersetErrorType.TEST_CONNECTION_INVALID_USERNAME_ERROR,
message='The username "testuser" does not exist.',
level=ErrorLevel.ERROR,
extra={"engine_name": "PostgreSQL"},
)
]
msg = 'psql: error: could not translate host name "locahost" to address: nodename nor servname provided, or not known'
result = PostgresEngineSpec.extract_errors(Exception(msg))
assert result == [
SupersetError(
error_type=SupersetErrorType.TEST_CONNECTION_INVALID_HOSTNAME_ERROR,
message='The hostname "locahost" cannot be resolved.',
level=ErrorLevel.ERROR,
extra={"engine_name": "PostgreSQL"},
)
]
msg = dedent(
"""
psql: error: could not connect to server: Connection refused
Is the server running on host "localhost" (::1) and accepting
TCP/IP connections on port 12345?
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP/IP connections on port 12345?
"""
)
result = PostgresEngineSpec.extract_errors(Exception(msg))
assert result == [
SupersetError(
error_type=SupersetErrorType.TEST_CONNECTION_PORT_CLOSED_ERROR,
message="Port 12345 on hostname localhost refused the connection.",
level=ErrorLevel.ERROR,
extra={"engine_name": "PostgreSQL"},
)
]
msg = dedent(
"""
psql: error: could not connect to server: Operation timed out
Is the server running on host "example.com" (93.184.216.34) and accepting
TCP/IP connections on port 12345?
"""
)
result = PostgresEngineSpec.extract_errors(Exception(msg))
assert result == [
SupersetError(
error_type=SupersetErrorType.TEST_CONNECTION_HOST_DOWN_ERROR,
message=(
"The host example.com might be down, "
"and can't be reached on port 12345"
),
level=ErrorLevel.ERROR,
extra={"engine_name": "PostgreSQL"},
)
]
# response with IP only
msg = dedent(
"""
psql: error: could not connect to server: Operation timed out
Is the server running on host "93.184.216.34" and accepting
TCP/IP connections on port 12345?
"""
)
result = PostgresEngineSpec.extract_errors(Exception(msg))
assert result == [
SupersetError(
error_type=SupersetErrorType.TEST_CONNECTION_HOST_DOWN_ERROR,
message=(
"The host 93.184.216.34 might be down, "
"and can't be reached on port 12345"
),
level=ErrorLevel.ERROR,
extra={"engine_name": "PostgreSQL"},
)
]