Skip to content

Commit bfd3e7d

Browse files
Fir 8302 implement integration tests for (#22)
* move unit tests to unit subfolder * select test * add tests for create, delete and insert * add tests for errors * add auth tests * update github job * fix comment * fix merge issues
1 parent ccff26c commit bfd3e7d

File tree

21 files changed

+412
-13
lines changed

21 files changed

+412
-13
lines changed

.github/workflows/python-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ jobs:
3939
4040
- name: Test with pytest
4141
run: |
42-
pytest
42+
pytest tests/unit

src/firebolt/db/cursor.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
from firebolt.client import Client
2727
from firebolt.common.exception import (
2828
CursorClosedError,
29-
QueryError,
29+
DataError,
30+
OperationalError,
31+
ProgrammingError,
3032
QueryNotRunError,
3133
)
3234
from firebolt.db._types import ColType, RawColType, parse_type, parse_value
@@ -36,7 +38,7 @@
3638

3739
ParameterType = Union[int, float, str, datetime, date, bool, Sequence]
3840

39-
JSON_OUTPUT_FORMAT = "FB_JSONCompactLimited"
41+
JSON_OUTPUT_FORMAT = "JSONCompact"
4042

4143

4244
class CursorState(Enum):
@@ -208,7 +210,17 @@ def _store_query_data(self, response: Response) -> None:
208210
# Parse data during fetch
209211
self._rows = query_data["data"]
210212
except (KeyError, JSONDecodeError) as err:
211-
raise QueryError(f"Invalid query data format: {str(err)}")
213+
raise DataError(f"Invalid query data format: {str(err)}")
214+
215+
def _raise_if_error(self, resp: Response) -> None:
216+
"""Raise a proper error if any"""
217+
if resp.status_code == codes.INTERNAL_SERVER_ERROR:
218+
raise OperationalError(
219+
f"Error executing query:\n{resp.read().decode('utf-8')}"
220+
)
221+
if resp.status_code == codes.FORBIDDEN:
222+
raise ProgrammingError(resp.read().decode("utf-8"))
223+
resp.raise_for_status()
212224

213225
def _reset(self) -> None:
214226
"""Clear all data stored from previous query."""
@@ -231,9 +243,7 @@ def _do_execute_request(
231243
content=query,
232244
)
233245

234-
if resp.status_code == codes.INTERNAL_SERVER_ERROR:
235-
raise QueryError(f"Error executing query:\n{resp.read().decode('utf-8')}")
236-
resp.raise_for_status()
246+
self._raise_if_error(resp)
237247
return resp
238248

239249
@check_not_closed
@@ -313,7 +323,7 @@ def fetchmany(self, size: Optional[int] = None) -> List[List[ColType]]:
313323
"""
314324
)
315325
with self._query_lock.gen_rlock():
316-
size = size or self.arraysize
326+
size = size if size is not None else self.arraysize
317327
left, right = self._get_next_range(size)
318328
assert self._rows is not None
319329
rows = self._rows[left:right]
File renamed without changes.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from datetime import date, datetime
2+
from logging import getLogger
3+
from os import environ
4+
from typing import List
5+
6+
from pytest import fixture
7+
8+
from firebolt.db import ARRAY, Connection
9+
from firebolt.db._types import ColType
10+
from firebolt.db.cursor import Column
11+
12+
LOGGER = getLogger(__name__)
13+
14+
ENGINE_URL_ENV = "ENGINE_URL"
15+
DATABASE_NAME_ENV = "DATABASE_NAME"
16+
USERNAME_ENV = "USERNAME"
17+
PASSWORD_ENV = "PASSWORD"
18+
API_ENDPOINT_ENV = "API_ENDPOINT"
19+
20+
21+
def must_env(var_name: str) -> str:
22+
assert var_name in environ, f"Expected {var_name} to be provided in environment"
23+
LOGGER.info(f"{var_name}: {environ[var_name]}")
24+
return environ[var_name]
25+
26+
27+
@fixture(scope="session")
28+
def engine_url() -> str:
29+
return must_env(ENGINE_URL_ENV)
30+
31+
32+
@fixture(scope="session")
33+
def database_name() -> str:
34+
return must_env(DATABASE_NAME_ENV)
35+
36+
37+
@fixture(scope="session")
38+
def username() -> str:
39+
return must_env(USERNAME_ENV)
40+
41+
42+
@fixture(scope="session")
43+
def password() -> str:
44+
return must_env(PASSWORD_ENV)
45+
46+
47+
@fixture(scope="session")
48+
def api_endpoint() -> str:
49+
return must_env(API_ENDPOINT_ENV)
50+
51+
52+
@fixture
53+
def connection(
54+
engine_url: str, database_name: str, username: str, password: str, api_endpoint: str
55+
) -> Connection:
56+
return Connection(
57+
engine_url, database_name, username, password, api_endpoint=api_endpoint
58+
)
59+
60+
61+
@fixture
62+
def all_types_query() -> str:
63+
return (
64+
"select 1 as uint8, 258 as uint16, 80000 as uint32, -30000 as int32,"
65+
"30000000000 as uint64, -30000000000 as int64, cast(1.23 AS FLOAT) as float32,"
66+
" 1.2345678901234 as float64, 'text' as \"string\", "
67+
"CAST('2021-03-28' AS DATE) as \"date\", "
68+
'CAST(\'2019-07-31 01:01:01\' AS DATETIME) as "datetime", true as "bool",'
69+
'[1,2,3,4] as "array", cast(null as int) as nullable'
70+
)
71+
72+
73+
@fixture
74+
def all_types_query_description() -> List[Column]:
75+
return [
76+
Column("uint8", int, None, None, None, None, None),
77+
Column("uint16", int, None, None, None, None, None),
78+
Column("uint32", int, None, None, None, None, None),
79+
Column("int32", int, None, None, None, None, None),
80+
Column("uint64", int, None, None, None, None, None),
81+
Column("int64", int, None, None, None, None, None),
82+
Column("float32", float, None, None, None, None, None),
83+
Column("float64", float, None, None, None, None, None),
84+
Column("string", str, None, None, None, None, None),
85+
Column("date", date, None, None, None, None, None),
86+
Column("datetime", datetime, None, None, None, None, None),
87+
Column("bool", int, None, None, None, None, None),
88+
Column("array", ARRAY(int), None, None, None, None, None),
89+
Column("nullable", str, None, None, None, None, None),
90+
]
91+
92+
93+
@fixture
94+
def all_types_query_response() -> List[ColType]:
95+
return [
96+
[
97+
1,
98+
258,
99+
80000,
100+
-30000,
101+
30000000000,
102+
-30000000000,
103+
1.23,
104+
1.23456789012,
105+
"text",
106+
date(2021, 3, 28),
107+
datetime(2019, 7, 31, 1, 1, 1),
108+
1,
109+
[1, 2, 3, 4],
110+
None,
111+
]
112+
]
113+
114+
115+
@fixture
116+
def create_drop_description() -> List[Column]:
117+
return [
118+
Column("host", str, None, None, None, None, None),
119+
Column("port", int, None, None, None, None, None),
120+
Column("status", int, None, None, None, None, None),
121+
Column("error", str, None, None, None, None, None),
122+
Column("num_hosts_remaining", int, None, None, None, None, None),
123+
Column("num_hosts_active", int, None, None, None, None, None),
124+
]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from time import time
2+
3+
from pytest import raises
4+
5+
from firebolt.common.exception import AuthenticationError
6+
from firebolt.db import Connection
7+
8+
9+
def test_refresh_token(connection: Connection) -> None:
10+
"""Auth refreshes token on expiration/invalidation"""
11+
with connection.cursor() as c:
12+
# Works fine
13+
c.execute("show tables")
14+
15+
# Invalidate the token
16+
c._client.auth._token += "_"
17+
18+
# Still works fine
19+
c.execute("show tables")
20+
21+
old = c._client.auth.token
22+
c._client.auth._expires = int(time()) - 1
23+
24+
# Still works fine
25+
c.execute("show tables")
26+
27+
assert c._client.auth.token != old, "Auth didn't update token on expiration"
28+
29+
30+
def test_credentials_invalidation(connection: Connection) -> None:
31+
"""Auth raises Authentication Error on credentials invalidation"""
32+
with connection.cursor() as c:
33+
# Works fine
34+
c.execute("show tables")
35+
36+
# Invalidate the token
37+
c._client.auth._token += "_"
38+
c._client.auth.username += "_"
39+
c._client.auth.password += "_"
40+
41+
with raises(AuthenticationError) as exc_info:
42+
c.execute("show tables")
43+
44+
assert str(exc_info.value).startswith(
45+
"Failed to authenticate"
46+
), "Invalid authentication error message"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from httpx import ConnectError
2+
from pytest import raises
3+
4+
from firebolt.common.exception import (
5+
AuthenticationError,
6+
OperationalError,
7+
ProgrammingError,
8+
)
9+
from firebolt.db import Connection
10+
11+
12+
def test_invalid_credentials(
13+
engine_url: str, database_name: str, username: str, password: str, api_endpoint: str
14+
) -> None:
15+
"""Connection properly reacts to invalid credentials error"""
16+
connection = Connection(
17+
engine_url, database_name, username + "_", password + "_", api_endpoint
18+
)
19+
with raises(AuthenticationError) as exc_info:
20+
connection.cursor().execute("show tables")
21+
22+
assert str(exc_info.value).startswith(
23+
"Failed to authenticate"
24+
), "Invalid authentication error message"
25+
26+
27+
def test_engine_not_exists(
28+
engine_url: str, database_name: str, username: str, password: str, api_endpoint: str
29+
) -> None:
30+
"""Connection properly reacts to invalid engine url error"""
31+
connection = Connection(
32+
engine_url + "_", database_name, username, password, api_endpoint
33+
)
34+
with raises(ConnectError):
35+
connection.cursor().execute("show tables")
36+
37+
38+
def test_database_not_exists(
39+
engine_url: str, database_name: str, username: str, password: str, api_endpoint: str
40+
) -> None:
41+
"""Connection properly reacts to invalid database error"""
42+
new_db_name = database_name + "_"
43+
connection = Connection(engine_url, new_db_name, username, password, api_endpoint)
44+
with raises(ProgrammingError) as exc_info:
45+
connection.cursor().execute("show tables")
46+
47+
assert (
48+
str(exc_info.value) == f"Invalid database '{new_db_name}'"
49+
), "Invalid database name error message"
50+
51+
52+
def test_sql_error(connection: Connection) -> None:
53+
with connection.cursor() as c:
54+
with raises(OperationalError) as exc_info:
55+
c.execute("select ]")
56+
57+
assert str(exc_info.value).startswith(
58+
"Error executing query"
59+
), "Invalid SQL error message"

0 commit comments

Comments
 (0)