Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HRQB 18 - Data warehouse and Quickbase connection tests #27

Merged
merged 2 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,3 @@ publish-stage: ## Only use in an emergency
docker login -u AWS -p $$(aws ecr get-login-password --region us-east-1) $(ECR_URL_STAGE)
docker push $(ECR_URL_STAGE):latest
docker push $(ECR_URL_STAGE):`git describe --always`


## ---- Temporary Development Commands ---- ##

docker-build: # build Docker container
docker build --platform linux/amd64 \
-t hrqb-client:latest .

docker-bash: # bash shell to docker container
docker run --entrypoint /bin/bash -it hrqb-client
28 changes: 28 additions & 0 deletions hrqb/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from hrqb.config import Config, configure_logger, configure_sentry
from hrqb.tasks.pipelines import run_pipeline
from hrqb.utils import click_argument_to_dict
from hrqb.utils.data_warehouse import DWClient
from hrqb.utils.quickbase import QBClient

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -44,6 +46,32 @@ def ping(ctx: click.Context) -> None:
)


@main.command()
@click.pass_context
def test_connections(ctx: click.Context) -> None:
"""Test connectivity with Data Warehouse and Quickbase."""
all_success = True
for name, client in [("Data Warehouse", DWClient), ("Quickbase", QBClient)]:
try:
client().test_connection()
message = f"{name} connection successful"
logger.debug(message)
except Exception as exc: # noqa: BLE001
all_success = False
message = f"{name} connection failed: {exc}"
logger.error(message) # noqa: TRY400

message = "All connections OK" if all_success else "One or more connections failed"
logger.info(message)

logger.info(
"Total elapsed: %s",
str(
timedelta(seconds=perf_counter() - ctx.obj["START_TIME"]),
),
)


@click.group()
@click.option(
"-p",
Expand Down
15 changes: 15 additions & 0 deletions hrqb/utils/data_warehouse.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""hrqb.utils.data_warehouse"""

# ruff: noqa: PLR2004

import logging

import pandas as pd
Expand Down Expand Up @@ -45,6 +47,19 @@ def verify_connection_string_set(self) -> None:
)
raise AttributeError(message)

def test_connection(self) -> bool:
"""Test connection to Data Warehouse.

Executes a simple SQL statement that should return a simple dataframe if a valid
connection is established.
"""
if self.connection_string.startswith("sqlite"):
query = """select 1 as x, 2 as y"""
else:
query = """select 1 as x, 2 as y from dual"""
self.execute_query(query)
return True

def init_engine(self) -> None:
"""Instantiate a SQLAlchemy engine if not already configured and set."""
self.verify_connection_string_set()
Expand Down
12 changes: 12 additions & 0 deletions hrqb/utils/quickbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ class QBClient:
cache_results: bool = field(default=True)
_cache: dict = field(factory=dict, repr=False)

def test_connection(self) -> bool:
"""Test connection to Quickbase API.

Retrieves Quickbase app information and checks for known key to establish
successful authentication and app permissions.
"""
results = self.get_app_info()
if results.get("id") != Config().QUICKBASE_APP_ID:
message = f"API returned unexpected response: {results}"
raise ValueError(message)
return True

@property
def request_headers(self) -> dict:
return {
Expand Down
41 changes: 37 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@


@pytest.fixture(autouse=True)
def _test_env(monkeypatch, targets_directory, data_warehouse_connection_string):
def _test_env(request, monkeypatch, targets_directory, data_warehouse_connection_string):
if request.node.get_closest_marker("integration"):
return
monkeypatch.setenv("SENTRY_DSN", "None")
monkeypatch.setenv("WORKSPACE", "test")
monkeypatch.setenv("LUIGI_CONFIG_PATH", "hrqb/luigi.cfg")
Expand Down Expand Up @@ -205,9 +207,12 @@ def qbclient():


@pytest.fixture(scope="session", autouse=True)
def global_requests_mock():
with requests_mock.Mocker() as m:
yield m
def global_requests_mock(request):
if any(item.get_closest_marker("integration") for item in request.node.items):
yield
else:
with requests_mock.Mocker() as m:
yield m


@pytest.fixture
Expand Down Expand Up @@ -372,3 +377,31 @@ def data_warehouse_connection_string():
@pytest.fixture
def sqlite_dwclient():
return DWClient(connection_string="sqlite:///:memory:", engine_parameters={})


@pytest.fixture
def _qbclient_connection_test_success():
with mock.patch.object(QBClient, "test_connection") as mocked_connection_test:
mocked_connection_test.return_value = True
yield


@pytest.fixture
def _dwclient_connection_test_success():
with mock.patch.object(DWClient, "test_connection") as mocked_connection_test:
mocked_connection_test.return_value = True
yield


@pytest.fixture
def _dwclient_connection_test_raise_exception():
with mock.patch.object(DWClient, "test_connection") as mocked_connection_test:
mocked_connection_test.side_effect = Exception("Intentional Error Here")
yield


@pytest.fixture
def _qbclient_connection_test_raise_exception():
with mock.patch.object(QBClient, "test_connection") as mocked_connection_test:
mocked_connection_test.side_effect = Exception("Intentional Error Here")
yield
2 changes: 1 addition & 1 deletion tests/fixtures/qb_api_responses/getApp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"dateFormat": "MM-DD-YYYY",
"description": "My testing app",
"hasEveryoneOnTheInternet": true,
"id": "bpqe82s1",
"id": "qb-app-def456",
"name": "Testing App",
"securityProperties": {
"allowClone": false,
Expand Down
Empty file removed tests/oracle/__init__.py
Empty file.
9 changes: 0 additions & 9 deletions tests/oracle/test_connection.py

This file was deleted.

52 changes: 52 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pytest
from _pytest.logging import LogCaptureFixture
from click.testing import Result

Expand Down Expand Up @@ -134,3 +135,54 @@ def test_cli_pipeline_run_and_remove_data_success(caplog, runner):
]
for line in lines:
assert text_in_logs_or_stdout(line, caplog, result)


@pytest.mark.usefixtures(
"_dwclient_connection_test_success", "_qbclient_connection_test_success"
)
def test_cli_test_connections_success(caplog, runner):
caplog.set_level("DEBUG")
args = ["--verbose", "test-connections"]
result = runner.invoke(cli.main, args)
assert result.exit_code == OKAY_RESULT_CODE
lines = [
"Data Warehouse connection successful",
"Quickbase connection successful",
"All connections OK",
]
for line in lines:
assert text_in_logs_or_stdout(line, caplog, result)


@pytest.mark.usefixtures(
"_dwclient_connection_test_raise_exception", "_qbclient_connection_test_success"
)
def test_cli_test_connections_data_warehouse_connection_error(caplog, runner):
caplog.set_level("DEBUG")
args = ["--verbose", "test-connections"]
result = runner.invoke(cli.main, args)
assert result.exit_code == OKAY_RESULT_CODE
lines = [
"Data Warehouse connection failed: Intentional Error Here",
"Quickbase connection successful",
" One or more connections failed",
]
for line in lines:
assert text_in_logs_or_stdout(line, caplog, result)


@pytest.mark.usefixtures(
"_dwclient_connection_test_success", "_qbclient_connection_test_raise_exception"
)
def test_cli_test_connections_quickbase_connection_error(caplog, runner):
caplog.set_level("DEBUG")
args = ["--verbose", "test-connections"]
result = runner.invoke(cli.main, args)
assert result.exit_code == OKAY_RESULT_CODE
lines = [
"Data Warehouse connection successful",
"Quickbase connection failed: Intentional Error Here",
"One or more connections failed",
]
for line in lines:
assert text_in_logs_or_stdout(line, caplog, result)
32 changes: 32 additions & 0 deletions tests/test_connections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""tests.oracle.test_connections

These integration tests will bypass monkey-patched env vars and require real values
for the following env vars:
- QUICKBASE_API_URL
- QUICKBASE_API_TOKEN
- QUICKBASE_APP_ID
- DATA_WAREHOUSE_CONNECTION_STRING
"""

import oracledb
import pytest

from hrqb.utils.data_warehouse import DWClient
from hrqb.utils.quickbase import QBClient


@pytest.mark.integration()
def test_integration_oracle_client_installed_success():
oracledb.init_oracle_client()


@pytest.mark.integration()
def test_integration_data_warehouse_connection_success():
dwclient = DWClient()
assert dwclient.test_connection()


@pytest.mark.integration()
def test_integration_quickbase_api_connection_success():
qbclient = QBClient()
assert qbclient.test_connection()
11 changes: 11 additions & 0 deletions tests/test_data_warehouse_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pandas as pd
import pytest
from sqlalchemy.engine import Engine
from sqlalchemy.exc import ArgumentError

from hrqb.utils.data_warehouse import DWClient

Expand Down Expand Up @@ -72,3 +73,13 @@ def test_dwclient_execute_query_accepts_sql_parameters(sqlite_dwclient):
)
assert df.iloc[0].foo == foo_val
assert df.iloc[0].bar == bar_val


def test_dwclient_test_connection_success(sqlite_dwclient):
assert sqlite_dwclient.test_connection()


def test_dwclient_test_connection_connection_error(sqlite_dwclient):
sqlite_dwclient.connection_string = "bad-connection://nothing"
with pytest.raises(ArgumentError, match="Could not parse SQLAlchemy URL from string"):
sqlite_dwclient.test_connection()
20 changes: 20 additions & 0 deletions tests/test_qbclient_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pandas as pd
import pytest
import requests
from requests.exceptions import HTTPError
from requests.models import Response

from hrqb.exceptions import QBFieldNotFoundError
Expand Down Expand Up @@ -175,3 +176,22 @@ def test_qbclient_api_non_2xx_response_error(qbclient):
requests.RequestException, match="Quickbase API error - status 400"
):
assert qbclient.make_request(requests.get, "/always/fail")


def test_qbclient_test_connection_success(qbclient):
assert qbclient.test_connection()


def test_qbclient_test_connection_connection_error(qbclient):
error_message = "Invalid API token"
with mock.patch.object(type(qbclient), "make_request") as mocked_make_request:
mocked_make_request.side_effect = HTTPError(error_message)
with pytest.raises(HTTPError, match=error_message):
qbclient.test_connection()


def test_qbclient_test_connection_response_error(qbclient):
with mock.patch.object(type(qbclient), "make_request") as mocked_make_request:
mocked_make_request.return_value = {"msg": "this is not expected"}
with pytest.raises(ValueError, match="API returned unexpected response"):
qbclient.test_connection()
Loading