Skip to content

Commit

Permalink
Add new marker for skipping tests if using
Browse files Browse the repository at this point in the history
live backend.
  • Loading branch information
CasperWA committed Jan 9, 2024
1 parent 261d0ff commit 03fe631
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 47 deletions.
14 changes: 11 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ services:
ports:
- "${ENTITY_SERVICE_PORT:-8000}:80"
environment:
ENTITY_SERVICE_DEBUG:
ENTITY_SERVICE_BASE_URL: "${ENTITY_SERVICE_BASE_URL:-http://onto-ns.com/meta}"
ENTITY_SERVICE_BACKEND: mongodb
ENTITY_SERVICE_PRIVATE_SSL_KEY:
ENTITY_SERVICE_MONGO_URI: "mongodb://mongodb:27017"
ENTITY_SERVICE_MONGO_USER: ${ENTITY_SERVICE_MONGO_USER:-guest}
ENTITY_SERVICE_MONGO_PASSWORD: ${ENTITY_SERVICE_MONGO_PASSWORD:-guest}

ENTITY_SERVICE_MONGO_USER:
ENTITY_SERVICE_MONGO_PASSWORD:
ENTITY_SERVICE_MONGO_DB:
ENTITY_SERVICE_MONGO_COLLECTION:
ENTITY_SERVICE_ADMIN_BACKEND: admin
ENTITY_SERVICE_ADMIN_USER: ${ENTITY_SERVICE_ADMIN_USER:-root}
ENTITY_SERVICE_ADMIN_PASSWORD: ${ENTITY_SERVICE_ADMIN_PASSWORD:-root}
ENTITY_SERVICE_ADMIN_DB:
depends_on:
- mongodb
networks:
Expand Down
36 changes: 30 additions & 6 deletions tests/cli/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@
from pytest_httpx import HTTPXMock
from typer.testing import CliRunner

from ..conftest import GetBackendUserFixture


CLI_RESULT_FAIL_MESSAGE = "STDOUT:\n{stdout}\n\nSTDERR:\n{stderr}"


@pytest.mark.parametrize("input_method", ["cli_option", "stdin", "env"])
def test_login(
cli: CliRunner, input_method: Literal["cli_option", "stdin"], httpx_mock: HTTPXMock
cli: CliRunner,
input_method: Literal["cli_option", "stdin"],
httpx_mock: HTTPXMock,
get_backend_user: GetBackendUserFixture,
) -> None:
"""Test the `entities-service login` CLI command."""
import re
Expand All @@ -27,8 +32,10 @@ def test_login(
from dlite_entities_service.cli.main import APP
from dlite_entities_service.models.auth import Token

username = "testuser"
password = "testpassword"
backend_user = get_backend_user()

username = backend_user["username"]
password = backend_user["password"]

mock_token = Token(access_token="test_token")

Expand Down Expand Up @@ -74,6 +81,7 @@ def test_token_persistence(
httpx_mock: HTTPXMock,
static_dir: Path,
random_valid_entity: dict[str, Any],
get_backend_user: GetBackendUserFixture,
) -> None:
"""Test that the token is persisted to the config file."""
import re
Expand All @@ -82,8 +90,10 @@ def test_token_persistence(
from dlite_entities_service.cli.main import APP
from dlite_entities_service.models.auth import Token

username = "testuser"
password = "testpassword"
backend_user = get_backend_user(auth_role="readWrite")

username = backend_user["username"]
password = backend_user["password"]

mock_token = Token(access_token="test_token")

Expand Down Expand Up @@ -164,7 +174,9 @@ def test_token_persistence(
assert CONTEXT["token"] == mock_token, CONTEXT


def test_login_invalid_credentials(cli: CliRunner, httpx_mock: HTTPXMock) -> None:
def test_login_invalid_credentials(
cli: CliRunner, httpx_mock: HTTPXMock, get_backend_user: GetBackendUserFixture
) -> None:
"""Test that the command fails with invalid credentials."""
import re

Expand All @@ -174,6 +186,13 @@ def test_login_invalid_credentials(cli: CliRunner, httpx_mock: HTTPXMock) -> Non
username = "testuser"
password = "testpassword"

# Ensure the test credentials are not the same as the ones used for the backend
for auth_role in ["read", "readWrite"]:
assert (
username != get_backend_user(auth_role=auth_role)["username"]
or password != get_backend_user(auth_role=auth_role)["password"]
)

assert CONTEXT["token"] is None, CONTEXT

# Mock the HTTPX response
Expand Down Expand Up @@ -201,6 +220,7 @@ def test_login_invalid_credentials(cli: CliRunner, httpx_mock: HTTPXMock) -> Non
assert CONTEXT["token"] is None, CONTEXT


@pytest.mark.skip_if_live_backend("Does not raise HTTP errors in this case.")
def test_http_errors(cli: CliRunner, httpx_mock: HTTPXMock) -> None:
"""Ensure proper error messages are given if an HTTP error occurs."""
import re
Expand Down Expand Up @@ -242,6 +262,7 @@ def test_http_errors(cli: CliRunner, httpx_mock: HTTPXMock) -> None:
@pytest.mark.parametrize(
"return_status_code", [200, 500], ids=["OK", "Internal Server Error"]
)
@pytest.mark.skip_if_live_backend("Does not raise JSON decode errors in this case.")
def test_json_decode_errors(
cli: CliRunner, httpx_mock: HTTPXMock, return_status_code: Literal[200, 500]
) -> None:
Expand Down Expand Up @@ -278,6 +299,9 @@ def test_json_decode_errors(
assert CONTEXT["token"] is None, CONTEXT


@pytest.mark.skip_if_live_backend(
"Does not raise pydantic.ValidationErrors in this case."
)
def test_validation_error(cli: CliRunner, httpx_mock: HTTPXMock) -> None:
"""Ensure proper error messages are given if the response cannot be parsed as a
valid token."""
Expand Down
184 changes: 146 additions & 38 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,28 @@

if TYPE_CHECKING:
from pathlib import Path
from typing import Any, Literal, Protocol
from typing import Any, Literal, Protocol, TypedDict

from fastapi.testclient import TestClient
from httpx import Client

from dlite_entities_service.service.backend.admin import AdminBackend
from dlite_entities_service.service.backend.mongodb import MongoDBBackend

class UserFullInfoRoleDict(TypedDict):
"""Type for the user info dictionary with roles."""

role: str
db: str

class UserFullInfoDict(TypedDict):
"""Type for the full user info dictionary."""

username: str
password: str
full_name: str | None
roles: list[UserFullInfoRoleDict]

class ClientFixture(Protocol):
"""Protocol for the client fixture."""

Expand All @@ -23,6 +37,14 @@ def __call__(
) -> TestClient | Client:
...

class GetBackendUserFixture(Protocol):
"""Protocol for the get_backend_user fixture."""

def __call__(
self, auth_role: Literal["read", "readWrite"] | None = None
) -> UserFullInfoDict:
...


## Pytest configuration functions and hooks ##

Expand All @@ -45,6 +67,49 @@ def pytest_configure(config: pytest.Config) -> None:
live_backend: bool = config.getoption("--live-backend")
os.environ["ENTITY_SERVICE_BACKEND"] = "mongodb" if live_backend else "mongomock"

# Add extra markers
config.addinivalue_line(
"markers",
"skip_if_live_backend: mark test to skip it if using a live backend, "
"optionally specify a reason why it is skipped",
)


def pytest_collection_modifyitems(
config: pytest.Config, items: list[pytest.Item]
) -> None:
"""Called after collection has been performed. May filter or re-order the items
in-place."""
if config.getoption("--live-backend"):
# If the tests are run with a live backend, skip the tests marked with
# 'skip_if_live_backend'
prefix_reason = "Live backend used: {reason}"
default_reason = "Test is skipped when using a live backend"
for item in items:
if "skip_if_live_backend" in item.keywords:
marker: pytest.Mark = item.keywords["skip_if_live_backend"]

if marker.args:
assert len(marker.args) == 1, (
"The 'skip_if_live_backend' marker can only have one "
"argument."
)

reason = marker.args[0]
elif marker.kwargs and "reason" in marker.kwargs:
reason = marker.kwargs["reason"]
else:
reason = default_reason

assert isinstance(
reason, str
), "The reason for skipping the test must be a string."

# The marker does not have a reason
item.add_marker(
pytest.mark.skip(reason=prefix_reason.format(reason=reason))
)


def pytest_sessionstart(session: pytest.Session) -> None:
"""Called after the Session object has been created and before performing
Expand Down Expand Up @@ -162,10 +227,61 @@ def static_dir() -> Path:
return (Path(__file__).parent / "static").resolve()


@pytest.fixture(scope="session")
def get_backend_user() -> GetBackendUserFixture:
"""Return a function to get the backend user."""
from dlite_entities_service.service.config import CONFIG

def _get_backend_user(
auth_role: Literal["read", "readWrite"] | None = None
) -> UserFullInfoDict:
"""Return the backend user for the given authentication role."""
if auth_role is None:
auth_role = "read"

assert auth_role in (
"read",
"readWrite",
), "The authentication role must be either 'read' or 'readWrite'."

if auth_role == "read":
password = CONFIG.mongo_password.get_secret_value()
full_info_dict: UserFullInfoDict = {
"username": CONFIG.mongo_user,
"password": password.decode()
if isinstance(password, bytes)
else password,
"full_name": CONFIG.mongo_user,
"roles": [
{
"role": "read",
"db": CONFIG.mongo_db,
}
],
}
return full_info_dict

full_info_dict: UserFullInfoDict = {
"username": "test_write_user",
"password": "writer",
"full_name": "Test write user",
"roles": [
{
"role": "readWrite",
"db": CONFIG.mongo_db,
}
],
}
return full_info_dict

return _get_backend_user


@pytest.fixture(scope="session", autouse=True)
def _mongo_test_collection(static_dir: Path, live_backend: bool) -> None:
def _mongo_test_collection(
static_dir: Path, live_backend: bool, get_backend_user: GetBackendUserFixture
) -> None:
"""Add MongoDB test data to the chosen backend."""

import yaml

from dlite_entities_service.service.backend import Backends, get_backend
Expand Down Expand Up @@ -222,27 +338,23 @@ def _mongo_test_collection(static_dir: Path, live_backend: bool) -> None:
for user in admin_backend._db.command("usersInfo", usersInfo=1)["users"]
]

if CONFIG.mongo_user not in existing_users:
admin_backend._db.command(
"createUser",
createUser=CONFIG.mongo_user,
pwd=CONFIG.mongo_password.get_secret_value(),
roles=[{"role": "read", "db": CONFIG.mongo_db}],
)

if "test_write_user" not in existing_users:
admin_backend._db.command(
"createUser",
createUser="test_write_user",
pwd="writer",
roles=[{"role": "readWrite", "db": CONFIG.mongo_db}],
)

# Use backend settings with write rights
backend_settings = {
"mongo_username": "test_write_user",
"mongo_password": "writer",
}
for auth_role in ("read", "readWrite"):
user_full_info = get_backend_user(auth_role)
if user_full_info["username"] not in existing_users:
admin_backend._db.command(
"createUser",
createUser=user_full_info["username"],
pwd=user_full_info["password"],
customData={"full_name": user_full_info["full_name"]},
roles=user_full_info["roles"],
)

if auth_role == "readWrite":
# Use backend settings with write rights
backend_settings = {
"mongo_username": user_full_info["username"],
"mongo_password": user_full_info["password"],
}

# Get entities backend
backend: MongoDBBackend = get_backend(settings=backend_settings)
Expand Down Expand Up @@ -272,7 +384,9 @@ def _mock_lifespan(live_backend: bool, monkeypatch: pytest.MonkeyPatch) -> None:


@pytest.fixture()
def client(live_backend: bool) -> ClientFixture:
def client(
live_backend: bool, get_backend_user: GetBackendUserFixture
) -> ClientFixture:
"""Return the test client."""
import os

Expand All @@ -296,11 +410,10 @@ def _client(
if auth_role is None:
auth_role = "read"

if auth_role not in ("read", "readWrite"):
raise ValueError(
f"Invalid authentication role '{auth_role}'. Must be either 'read' or "
"'readWrite'."
)
assert auth_role in ("read", "readWrite"), (
f"Invalid authentication role {auth_role!r}. Must be either 'read' or "
"'readWrite'."
)

host, port = os.getenv("ENTITY_SERVICE_HOST", "localhost"), os.getenv(
"ENTITY_SERVICE_PORT", "8000"
Expand All @@ -311,20 +424,15 @@ def _client(
if port:
base_url += f":{port}"

username = CONFIG.mongo_user if auth_role == "read" else "test_write_user"
password = (
CONFIG.mongo_password.get_secret_value()
if auth_role == "read"
else "writer"
)
backend_user = get_backend_user(auth_role)

with Client(base_url=base_url) as temp_client:
response = temp_client.post(
"/_auth/token",
data={
"grant_type": "password",
"username": username,
"password": password,
"username": backend_user["username"],
"password": backend_user["password"],
},
)

Expand Down

0 comments on commit 03fe631

Please sign in to comment.