mirror of
https://github.com/ijaric/voice_assistant.git
synced 2025-05-24 14:33:26 +00:00
Merge pull request #42 from ijaric/tasks/#41_assistant_base_tests
[#41] Assistant: Добавление тестов из шаблона
This commit is contained in:
commit
bc88ceffec
1
.github/workflows/check-pr.yaml
vendored
1
.github/workflows/check-pr.yaml
vendored
|
@ -100,6 +100,7 @@ jobs:
|
||||||
|
|
||||||
- name: Test Package
|
- name: Test Package
|
||||||
env:
|
env:
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||||
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
|
JWT_SECRET_KEY: ${{ secrets.JWT_SECRET_KEY }}
|
||||||
POSTGRES_DRIVER: ${{ vars.POSTGRES_DRIVER }}
|
POSTGRES_DRIVER: ${{ vars.POSTGRES_DRIVER }}
|
||||||
|
|
|
@ -15,6 +15,10 @@ NGINX_PORT=80
|
||||||
API_HOST=0.0.0.0
|
API_HOST=0.0.0.0
|
||||||
API_PORT=8000
|
API_PORT=8000
|
||||||
|
|
||||||
|
TEST_API_PROTOCOL=http
|
||||||
|
TEST_API_HOST=api
|
||||||
|
TEST_API_PORT=8000
|
||||||
|
|
||||||
JWT_SECRET_KEY=v9LctjUWwol4XbvczPiLFMDtZ8aal7mm
|
JWT_SECRET_KEY=v9LctjUWwol4XbvczPiLFMDtZ8aal7mm
|
||||||
JWT_ALGORITHM=HS256
|
JWT_ALGORITHM=HS256
|
||||||
|
|
||||||
|
|
18
src/assistant/Dockerfile.tests
Normal file
18
src/assistant/Dockerfile.tests
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
FROM python:3.11
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
|
||||||
|
WORKDIR /opt/app
|
||||||
|
|
||||||
|
COPY pyproject.toml ./
|
||||||
|
COPY poetry.lock ./
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& pip install poetry \
|
||||||
|
&& poetry config virtualenvs.create false \
|
||||||
|
&& poetry install --no-dev
|
||||||
|
|
||||||
|
COPY tests tests
|
||||||
|
COPY lib lib
|
||||||
|
|
||||||
|
CMD ["pytest"]
|
|
@ -1,3 +1,3 @@
|
||||||
include ../../common_makefile.mk
|
include ../../common_makefile.mk
|
||||||
|
|
||||||
PROJECT_FOLDERS = bin lib tests
|
PROJECT_FOLDERS = bin lib tests
|
||||||
|
|
56
src/assistant/docker-compose.tests.yml
Normal file
56
src/assistant/docker-compose.tests.yml
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:15.2
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: ${POSTGRES_NAME}
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
expose:
|
||||||
|
- "${POSTGRES_PORT}"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data/
|
||||||
|
networks:
|
||||||
|
- backend_network
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
container_name: api
|
||||||
|
image: fastapi_app
|
||||||
|
restart: always
|
||||||
|
entrypoint: ["/opt/app/entrypoint.sh"]
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
expose:
|
||||||
|
- "${API_PORT}"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- backend_network
|
||||||
|
- api_network
|
||||||
|
|
||||||
|
tests:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: "Dockerfile.tests"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
- api
|
||||||
|
networks:
|
||||||
|
- api_network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
api_network:
|
||||||
|
driver: bridge
|
||||||
|
backend_network:
|
||||||
|
driver: bridge
|
20
src/assistant/poetry.lock
generated
20
src/assistant/poetry.lock
generated
|
@ -1507,6 +1507,24 @@ pluggy = ">=0.12,<2.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "0.21.1"
|
||||||
|
description = "Pytest support for asyncio"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
|
||||||
|
{file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=7.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
|
||||||
|
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -2051,4 +2069,4 @@ multidict = ">=4.0"
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "64e70d8e6e21863567fed017ba25f061ab0af9b99b6281b50ff6eceb7bc4cb7b"
|
content-hash = "9d9f56a0892e3eb62cadf0061462fe1b8a9fd8aa404761e331d485a6a05a531c"
|
||||||
|
|
|
@ -26,11 +26,14 @@ dill = "^0.3.7"
|
||||||
fastapi = "0.103.1"
|
fastapi = "0.103.1"
|
||||||
greenlet = "^2.0.2"
|
greenlet = "^2.0.2"
|
||||||
httpx = "^0.25.0"
|
httpx = "^0.25.0"
|
||||||
|
multidict = "^6.0.4"
|
||||||
openai = "^0.28.1"
|
openai = "^0.28.1"
|
||||||
orjson = "3.9.7"
|
orjson = "3.9.7"
|
||||||
psycopg2-binary = "^2.9.9"
|
psycopg2-binary = "^2.9.9"
|
||||||
pydantic = {extras = ["email"], version = "^2.3.0"}
|
pydantic = {extras = ["email"], version = "^2.3.0"}
|
||||||
pydantic-settings = "^2.0.3"
|
pydantic-settings = "^2.0.3"
|
||||||
|
pytest = "^7.4.2"
|
||||||
|
pytest-asyncio = "^0.21.1"
|
||||||
python = "^3.11"
|
python = "^3.11"
|
||||||
python-jose = "^3.3.0"
|
python-jose = "^3.3.0"
|
||||||
python-magic = "^0.4.27"
|
python-magic = "^0.4.27"
|
||||||
|
@ -90,6 +93,7 @@ variable-rgx = "^_{0,2}[a-z][a-z0-9_]*$"
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
exclude = [
|
exclude = [
|
||||||
|
".pytest_cache",
|
||||||
".venv"
|
".venv"
|
||||||
]
|
]
|
||||||
pythonPlatform = "All"
|
pythonPlatform = "All"
|
||||||
|
|
70
src/assistant/tests/conftest.py
Normal file
70
src/assistant/tests/conftest.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import asyncio
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import fastapi
|
||||||
|
import httpx
|
||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
import lib.app as lib_app
|
||||||
|
import tests.core.settings as tests_core_settings
|
||||||
|
import tests.functional.models as functional_models
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture # type: ignore[reportUntypedFunctionDecorator]
|
||||||
|
async def http_client(
|
||||||
|
base_url: str = tests_core_settings.tests_settings.api.get_api_url,
|
||||||
|
) -> typing.AsyncGenerator[httpx.AsyncClient, typing.Any]:
|
||||||
|
session = httpx.AsyncClient(base_url=base_url)
|
||||||
|
yield session
|
||||||
|
await session.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture # type: ignore[reportUntypedFunctionDecorator]
|
||||||
|
async def make_request(http_client: httpx.AsyncClient):
|
||||||
|
async def inner(
|
||||||
|
api_method: str = "",
|
||||||
|
method: functional_models.MethodsEnum = functional_models.MethodsEnum.GET,
|
||||||
|
headers: dict[str, str] = tests_core_settings.tests_settings.api.headers,
|
||||||
|
body: dict[str, typing.Any] | None = None,
|
||||||
|
jwt_token: str | None = None,
|
||||||
|
) -> functional_models.HTTPResponse:
|
||||||
|
if jwt_token is not None:
|
||||||
|
headers["Authorization"] = f"Bearer {jwt_token}"
|
||||||
|
|
||||||
|
client_params = {"json": body, "headers": headers}
|
||||||
|
if method == functional_models.MethodsEnum.GET:
|
||||||
|
del client_params["json"]
|
||||||
|
|
||||||
|
response = await getattr(http_client, method.value)(api_method, **client_params)
|
||||||
|
return functional_models.HTTPResponse(
|
||||||
|
body=response.json(),
|
||||||
|
headers=response.headers,
|
||||||
|
status_code=response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="session") # type: ignore[reportUntypedFunctionDecorator]
|
||||||
|
def app() -> fastapi.FastAPI:
|
||||||
|
settings = lib_app.Settings()
|
||||||
|
application = lib_app.Application.from_settings(settings)
|
||||||
|
fastapi_app = application._fastapi_app # type: ignore[reportPrivateUsage]
|
||||||
|
return fastapi_app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture # type: ignore[reportUntypedFunctionDecorator]
|
||||||
|
async def app_http_client(
|
||||||
|
app: fastapi.FastAPI,
|
||||||
|
base_url: str = tests_core_settings.tests_settings.api.get_api_url,
|
||||||
|
) -> typing.AsyncGenerator[httpx.AsyncClient, typing.Any]:
|
||||||
|
session = httpx.AsyncClient(app=app, base_url=base_url)
|
||||||
|
yield session
|
||||||
|
await session.aclose()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="session") # type: ignore[reportUntypedFunctionDecorator]
|
||||||
|
def event_loop():
|
||||||
|
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||||
|
yield loop
|
||||||
|
loop.close()
|
5
src/assistant/tests/core/__init__.py
Normal file
5
src/assistant/tests/core/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from .settings import *
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"tests_settings",
|
||||||
|
]
|
17
src/assistant/tests/core/settings.py
Normal file
17
src/assistant/tests/core/settings.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import pydantic
|
||||||
|
import pydantic_settings
|
||||||
|
|
||||||
|
import tests.core.split_settings as app_split_settings
|
||||||
|
|
||||||
|
|
||||||
|
class TestsSettings(pydantic_settings.BaseSettings):
|
||||||
|
api: app_split_settings.ApiSettings = pydantic.Field(default_factory=lambda: app_split_settings.ApiSettings())
|
||||||
|
postgres: app_split_settings.PostgresSettings = pydantic.Field(
|
||||||
|
default_factory=lambda: app_split_settings.PostgresSettings()
|
||||||
|
)
|
||||||
|
project: app_split_settings.ProjectSettings = pydantic.Field(
|
||||||
|
default_factory=lambda: app_split_settings.ProjectSettings()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
tests_settings = TestsSettings()
|
9
src/assistant/tests/core/split_settings/__init__.py
Normal file
9
src/assistant/tests/core/split_settings/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from .api import *
|
||||||
|
from .postgres import *
|
||||||
|
from .project import *
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ApiSettings",
|
||||||
|
"PostgresSettings",
|
||||||
|
"ProjectSettings",
|
||||||
|
]
|
23
src/assistant/tests/core/split_settings/api.py
Normal file
23
src/assistant/tests/core/split_settings/api.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import pydantic
|
||||||
|
import pydantic_settings
|
||||||
|
|
||||||
|
import lib.app.split_settings.utils as app_split_settings_utils
|
||||||
|
|
||||||
|
|
||||||
|
class ApiSettings(pydantic_settings.BaseSettings):
|
||||||
|
model_config = pydantic_settings.SettingsConfigDict(
|
||||||
|
env_file=app_split_settings_utils.ENV_PATH,
|
||||||
|
env_prefix="TEST_API_",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
protocol: str = "http"
|
||||||
|
host: str = "0.0.0.0"
|
||||||
|
port: int = 8000
|
||||||
|
headers: dict[str, str] = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
@pydantic.computed_field
|
||||||
|
@property
|
||||||
|
def get_api_url(self) -> str:
|
||||||
|
return f"{self.protocol}://{self.host}:{self.port}/api/v1"
|
42
src/assistant/tests/core/split_settings/postgres.py
Normal file
42
src/assistant/tests/core/split_settings/postgres.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import pydantic
|
||||||
|
import pydantic_settings
|
||||||
|
|
||||||
|
import lib.app.split_settings.utils as app_split_settings_utils
|
||||||
|
|
||||||
|
|
||||||
|
class PostgresSettings(pydantic_settings.BaseSettings):
|
||||||
|
model_config = pydantic_settings.SettingsConfigDict(
|
||||||
|
env_file=app_split_settings_utils.ENV_PATH,
|
||||||
|
env_prefix="POSTGRES_",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
name: str = "test_database_name"
|
||||||
|
host: str = "localhost"
|
||||||
|
port: int = 5432
|
||||||
|
user: str = "app"
|
||||||
|
password: pydantic.SecretStr = pydantic.Field(
|
||||||
|
default=...,
|
||||||
|
validation_alias=pydantic.AliasChoices("password", "postgres_password"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_uri_async(self) -> str:
|
||||||
|
db_uri: str = "postgresql+asyncpg://{pg_user}:{pg_pass}@{pg_host}/{pg_dbname}".format(
|
||||||
|
pg_user=self.user,
|
||||||
|
pg_pass=self.password.get_secret_value(),
|
||||||
|
pg_host=self.host,
|
||||||
|
pg_dbname=self.name,
|
||||||
|
)
|
||||||
|
return db_uri
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db_uri_sync(self) -> str:
|
||||||
|
db_uri: str = "postgresql://{pg_user}:{pg_pass}@{pg_host}/{pg_dbname}".format(
|
||||||
|
pg_user=self.user,
|
||||||
|
pg_pass=self.password.get_secret_value(),
|
||||||
|
pg_host=self.host,
|
||||||
|
pg_dbname=self.name,
|
||||||
|
)
|
||||||
|
return db_uri
|
15
src/assistant/tests/core/split_settings/project.py
Normal file
15
src/assistant/tests/core/split_settings/project.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import pydantic
|
||||||
|
import pydantic_settings
|
||||||
|
|
||||||
|
import lib.app.split_settings.utils as app_split_settings_utils
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectSettings(pydantic_settings.BaseSettings):
|
||||||
|
model_config = pydantic_settings.SettingsConfigDict(
|
||||||
|
env_file=app_split_settings_utils.ENV_PATH,
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
debug: bool = False
|
||||||
|
jwt_secret_key: pydantic.SecretStr = pydantic.Field(default=..., validation_alias="jwt_secret_key")
|
4
src/assistant/tests/core/split_settings/utils.py
Normal file
4
src/assistant/tests/core/split_settings/utils.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
BASE_PATH = pathlib.Path(__file__).parent.parent.parent.parent.parent.resolve()
|
||||||
|
ENV_PATH = BASE_PATH / ".env"
|
0
src/assistant/tests/functional/__init__.py
Normal file
0
src/assistant/tests/functional/__init__.py
Normal file
7
src/assistant/tests/functional/models/__init__.py
Normal file
7
src/assistant/tests/functional/models/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from .http import *
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"HTTPResponse",
|
||||||
|
"MakeResponseCallableType",
|
||||||
|
"MethodsEnum",
|
||||||
|
]
|
35
src/assistant/tests/functional/models/http.py
Normal file
35
src/assistant/tests/functional/models/http.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import dataclasses
|
||||||
|
import enum
|
||||||
|
import typing
|
||||||
|
|
||||||
|
import multidict
|
||||||
|
|
||||||
|
import tests.core.settings as functional_settings
|
||||||
|
|
||||||
|
|
||||||
|
class MethodsEnum(enum.Enum):
|
||||||
|
GET = "get"
|
||||||
|
POST = "post"
|
||||||
|
PUT = "put"
|
||||||
|
DELETE = "delete"
|
||||||
|
PATCH = "patch"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class HTTPResponse:
|
||||||
|
body: dict[str, typing.Any] | str
|
||||||
|
headers: multidict.CIMultiDictProxy[str]
|
||||||
|
status_code: int
|
||||||
|
|
||||||
|
|
||||||
|
class MakeResponseCallableType(typing.Protocol):
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
api_method: str = "",
|
||||||
|
url: str = functional_settings.tests_settings.api.get_api_url,
|
||||||
|
method: MethodsEnum = MethodsEnum.GET,
|
||||||
|
headers: dict[str, str] = functional_settings.tests_settings.api.headers,
|
||||||
|
body: dict[str, typing.Any] | None = None,
|
||||||
|
jwt_token: str | None = None,
|
||||||
|
) -> HTTPResponse:
|
||||||
|
...
|
0
src/assistant/tests/functional/src/__init__.py
Normal file
0
src/assistant/tests/functional/src/__init__.py
Normal file
17
src/assistant/tests/functional/src/test_health.py
Normal file
17
src/assistant/tests/functional/src/test_health.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# import http
|
||||||
|
|
||||||
|
# import pytest
|
||||||
|
|
||||||
|
# import tests.functional.models as tests_functional_models
|
||||||
|
|
||||||
|
# pytestmark = [pytest.mark.asyncio]
|
||||||
|
|
||||||
|
|
||||||
|
# async def test_health(
|
||||||
|
# make_request: tests_functional_models.MakeResponseCallableType,
|
||||||
|
# ):
|
||||||
|
# response = await make_request(
|
||||||
|
# method=tests_functional_models.MethodsEnum.GET,
|
||||||
|
# api_method=f"/health/",
|
||||||
|
# )
|
||||||
|
# assert response.status_code == http.HTTPStatus.OK
|
0
src/assistant/tests/functional/testdata/__init__.py
vendored
Normal file
0
src/assistant/tests/functional/testdata/__init__.py
vendored
Normal file
3
src/assistant/tests/pytest.ini
Normal file
3
src/assistant/tests/pytest.ini
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[pytest]
|
||||||
|
log_format = %(asctime)s %(levelname)s %(message)s
|
||||||
|
log_date_format = %Y-%m-%d %H:%M:%S
|
0
src/assistant/tests/unit/__init__.py
Normal file
0
src/assistant/tests/unit/__init__.py
Normal file
0
src/assistant/tests/unit/src/__init__.py
Normal file
0
src/assistant/tests/unit/src/__init__.py
Normal file
11
src/assistant/tests/unit/src/test_health.py
Normal file
11
src/assistant/tests/unit/src/test_health.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import http
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.asyncio]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_health(app_http_client: httpx.AsyncClient) -> None:
|
||||||
|
response = await app_http_client.get("/health/")
|
||||||
|
assert response.status_code == http.HTTPStatus.OK
|
Loading…
Reference in New Issue
Block a user