[tests] migrate to tests without docker containers

This commit is contained in:
Piotr Domański 2024-05-20 12:10:17 +02:00
parent c41620a02f
commit 98fa2187d4
13 changed files with 146 additions and 189 deletions

View file

@ -1,30 +0,0 @@
version: '3'
networks:
fooder_test:
driver: bridge
services:
database:
restart: unless-stopped
image: postgres
networks:
- fooder_test
env_file:
- .env.test
api:
restart: unless-stopped
image: registry.domandoman.xyz/fooder/api
build:
dockerfile: Dockerfile
context: .
networks:
- fooder_test
env_file:
- .env.test
volumes:
- ./fooder:/opt/fooder/fooder
ports:
- "8000:8000"
command: "uvicorn fooder.app:app --host 0.0.0.0 --port 8000 --reload"

View file

@ -7,7 +7,7 @@ DB_URI="postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database/${PO
ECHO_SQL=0 ECHO_SQL=0
SECRET_KEY="${SECRET_KEY}" # generate with $ openssl rand -hex 32 SECRET_KEY="${SECRET_KEY}" # generate with $ openssl rand -hex 32
REFRESH_SECRET_KEY="${REFRESH_SECRET}" # generate with $ openssl rand -hex 32 REFRESH_SECRET_KEY="${REFRESH_SECRET_KEY}" # generate with $ openssl rand -hex 32
ALGORITHM="HS256" ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=30 ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=30 REFRESH_TOKEN_EXPIRE_DAYS=30

View file

@ -16,7 +16,9 @@ if __name__ == "__main__":
from .settings import Settings from .settings import Settings
settings = Settings() settings = Settings()
engine = sqlalchemy.create_engine(settings.DB_URI.replace("+asyncpg", "")) engine = sqlalchemy.create_engine(
settings.DB_URI.replace("+asyncpg", "").replace("+aiosqlite", "")
)
if args.create_tables: if args.create_tables:
Base.metadata.create_all(engine) Base.metadata.create_all(engine)

View file

@ -9,10 +9,19 @@ from .settings import Settings
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
settings = Settings.parse_obj({}) settings = Settings.parse_obj({})
if settings.DB_URI.startswith("sqlite"):
settings.DB_URI = settings.DB_URI + "?check_same_thread=False"
"""
Asynchronous PostgreSQL database engine.
"""
async_engine = create_async_engine( async_engine = create_async_engine(
settings.DB_URI, settings.DB_URI,
pool_pre_ping=True, pool_pre_ping=True,
echo=settings.ECHO_SQL, echo=settings.ECHO_SQL,
connect_args=(
{"check_same_thread": False} if settings.DB_URI.startswith("sqlite") else {}
),
) )
AsyncSessionLocal = async_sessionmaker( AsyncSessionLocal = async_sessionmaker(
bind=async_engine, bind=async_engine,

View file

@ -3,3 +3,9 @@ from .user import * # noqa
from .product import * # noqa from .product import * # noqa
from .meal import * # noqa from .meal import * # noqa
from .entry import * # noqa from .entry import * # noqa
import pytest
@pytest.fixture
def anyio_backend():
return "asyncio"

View file

@ -1,31 +1,17 @@
import requests from fooder.app import app
from httpx import AsyncClient
import pytest import pytest
import os import httpx
import yaml
def get_api_url(service_name) -> str:
with open("docker-compose.test.yml") as f:
config = yaml.safe_load(f)
port = config["services"][service_name]["ports"][0].split(":")[0]
return f"http://localhost:{port}/"
class Client: class Client:
def __init__( def __init__(
self, self,
base_url: str,
username: str | None = None, username: str | None = None,
password: str | None = None, password: str | None = None,
): ):
self.base_url = os.path.join(base_url, "api") self.client = AsyncClient(app=app, base_url="http://testserver/api")
self.session = requests.Session() self.client.headers["Accept"] = "application/json"
self.session.headers["Accept"] = "application/json"
if username and password:
self.login(username, password, True)
def set_token(self, token: str) -> None: def set_token(self, token: str) -> None:
"""set_token. """set_token.
@ -34,14 +20,14 @@ class Client:
:type token: str :type token: str
:rtype: None :rtype: None
""" """
self.session.headers["Authorization"] = "Bearer " + token self.client.headers["Authorization"] = "Bearer " + token
def create_user(self, username: str, password: str) -> None: async def create_user(self, username: str, password: str) -> None:
data = {"username": username, "password": password} data = {"username": username, "password": password}
response = self.post("user", json=data) response = await self.post("user", json=data)
response.raise_for_status() response.raise_for_status()
def login(self, username: str, password: str, force_login: bool) -> None: async def login(self, username: str, password: str, force_login: bool) -> None:
"""login. """login.
:param username: :param username:
@ -54,40 +40,40 @@ class Client:
""" """
data = {"username": username, "password": password} data = {"username": username, "password": password}
response = self.post("token", data=data) response = await self.post("token", data=data)
if response.status_code != 200: if response.status_code != 200:
if force_login: if force_login:
self.create_user(username, password) await self.create_user(username, password)
return self.login(username, password, False) return await self.login(username, password, False)
else: else:
raise Exception(f"Could not login as {username}") raise Exception(
f"Could not login as {username}! Detail: {response.text}"
)
result = response.json() result = response.json()
self.set_token(result["access_token"]) self.set_token(result["access_token"])
def get(self, path: str, **kwargs) -> requests.Response: async def get(self, path: str, **kwargs) -> httpx.Response:
return self.session.get(os.path.join(self.base_url, path), **kwargs) return await self.client.get(path, **kwargs)
def delete(self, path: str, **kwargs) -> requests.Response: async def delete(self, path: str, **kwargs) -> httpx.Response:
return self.session.delete(os.path.join(self.base_url, path), **kwargs) return await self.client.delete(path, **kwargs)
def post(self, path: str, **kwargs) -> requests.Response: async def post(self, path: str, **kwargs) -> httpx.Response:
return self.session.post(os.path.join(self.base_url, path), **kwargs) return await self.client.post(path, **kwargs)
def patch(self, path: str, **kwargs) -> requests.Response: async def patch(self, path: str, **kwargs) -> httpx.Response:
return self.session.patch(os.path.join(self.base_url, path), **kwargs) return await self.client.patch(path, **kwargs)
@pytest.fixture @pytest.fixture
def unauthorized_client() -> Client: def unauthorized_client() -> Client:
return Client(get_api_url("api")) return Client()
@pytest.fixture @pytest.fixture
def client(user_payload) -> Client: async def client(user_payload) -> Client:
return Client( client = Client()
get_api_url("api"), await client.login(user_payload["username"], user_payload["password"], True)
username=user_payload["username"], return client
password=user_payload["password"],
)

View file

@ -2,10 +2,10 @@ import datetime
import pytest import pytest
@pytest.mark.dependency() @pytest.mark.anyio
def test_get_diary(client): async def test_get_diary(client):
today = datetime.date.today().isoformat() today = datetime.date.today().isoformat()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
assert response.json()["date"] == today assert response.json()["date"] == today
@ -13,52 +13,56 @@ def test_get_diary(client):
assert len(response.json()["meals"]) == 1 assert len(response.json()["meals"]) == 1
@pytest.mark.dependency(depends=["test_get_diary"]) @pytest.mark.anyio
def test_diary_add_meal(client, meal_payload_factory): async def test_diary_add_meal(client, meal_payload_factory):
today = datetime.date.today().isoformat() today = datetime.date.today().isoformat()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
diary_id = response.json()["id"] diary_id = response.json()["id"]
meal_order = len(response.json()["meals"]) + 1 meal_order = len(response.json()["meals"]) + 1
response = client.post("meal", json=meal_payload_factory(diary_id, meal_order)) response = await client.post(
"meal", json=meal_payload_factory(diary_id, meal_order)
)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
@pytest.mark.dependency(depends=["test_diary_add_meal"]) @pytest.mark.anyio
def test_diary_delete_meal(client): async def test_diary_delete_meal(client):
today = datetime.date.today().isoformat() today = datetime.date.today().isoformat()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
meals_amount = len(response.json()["meals"]) meals_amount = len(response.json()["meals"])
meal_id = response.json()["meals"][0]["id"] meal_id = response.json()["meals"][0]["id"]
response = client.delete(f"meal/{meal_id}") response = await client.delete(f"meal/{meal_id}")
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
assert len(response.json()["meals"]) == meals_amount - 1 assert len(response.json()["meals"]) == meals_amount - 1
@pytest.mark.dependency(depends=["test_get_diary"]) @pytest.mark.anyio
def test_diary_add_entry(client, product_payload_factory, entry_payload_factory): async def test_diary_add_entry(client, product_payload_factory, entry_payload_factory):
today = datetime.date.today().isoformat() today = datetime.date.today().isoformat()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
meal_id = response.json()["meals"][0]["id"] meal_id = response.json()["meals"][0]["id"]
product_id = client.post("product", json=product_payload_factory()).json()["id"] product_id = (await client.post("product", json=product_payload_factory())).json()[
"id"
]
entry_payload = entry_payload_factory(meal_id, product_id, 100.0) entry_payload = entry_payload_factory(meal_id, product_id, 100.0)
response = client.post("entry", json=entry_payload) response = await client.post("entry", json=entry_payload)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
@pytest.mark.dependency(depends=["test_diary_add_entry"]) @pytest.mark.anyio
def test_diary_edit_entry(client, entry_payload_factory): async def test_diary_edit_entry(client, entry_payload_factory):
today = datetime.date.today().isoformat() today = datetime.date.today().isoformat()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
entry = response.json()["meals"][0]["entries"][0] entry = response.json()["meals"][0]["entries"][0]
id_ = entry["id"] id_ = entry["id"]
@ -66,21 +70,21 @@ def test_diary_edit_entry(client, entry_payload_factory):
entry["meal_id"], entry["product"]["id"], entry["grams"] + 100.0 entry["meal_id"], entry["product"]["id"], entry["grams"] + 100.0
) )
response = client.patch(f"entry/{id_}", json=entry_payload) response = await client.patch(f"entry/{id_}", json=entry_payload)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
assert response.json()["grams"] == entry_payload["grams"] assert response.json()["grams"] == entry_payload["grams"]
@pytest.mark.dependency(depends=["test_diary_add_entry"]) @pytest.mark.anyio
def test_diary_delete_entry(client): async def test_diary_delete_entry(client):
today = datetime.date.today().isoformat() today = datetime.date.today().isoformat()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
entry_id = response.json()["meals"][0]["entries"][0]["id"] entry_id = response.json()["meals"][0]["entries"][0]["id"]
response = client.delete(f"entry/{entry_id}") response = await client.delete(f"entry/{entry_id}")
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
deleted_entries = [ deleted_entries = [
entry entry

View file

@ -2,38 +2,42 @@ import datetime
import pytest import pytest
@pytest.mark.dependency() @pytest.mark.anyio
def test_create_meal( async def test_create_meal(
client, meal_payload_factory, product_payload_factory, entry_payload_factory client, meal_payload_factory, product_payload_factory, entry_payload_factory
): ):
today = datetime.date.today().isoformat() today = datetime.date.today().isoformat()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
diary_id = response.json()["id"] diary_id = response.json()["id"]
meal_order = len(response.json()["meals"]) + 1 meal_order = len(response.json()["meals"]) + 1
response = client.post("meal", json=meal_payload_factory(diary_id, meal_order)) response = await client.post(
"meal", json=meal_payload_factory(diary_id, meal_order)
)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
meal_id = response.json()["id"] meal_id = response.json()["id"]
product_id = client.post("product", json=product_payload_factory()).json()["id"] product_id = (await client.post("product", json=product_payload_factory())).json()[
"id"
]
entry_payload = entry_payload_factory(meal_id, product_id, 100.0) entry_payload = entry_payload_factory(meal_id, product_id, 100.0)
response = client.post("entry", json=entry_payload) response = await client.post("entry", json=entry_payload)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
@pytest.mark.dependency(depends=["test_create_meal"]) @pytest.mark.anyio
def test_save_meal(client, meal_save_payload): async def test_save_meal(client, meal_save_payload):
today = datetime.date.today().isoformat() today = datetime.date.today().isoformat()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
meal = response.json()["meals"][0] meal = response.json()["meals"][0]
meal_id = meal["id"] meal_id = meal["id"]
save_payload = meal_save_payload(meal_id) save_payload = meal_save_payload(meal_id)
response = client.post(f"meal/{meal_id}/save", json=save_payload) response = await client.post(f"meal/{meal_id}/save", json=save_payload)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
preset = response.json() preset = response.json()
@ -45,27 +49,27 @@ def test_save_meal(client, meal_save_payload):
assert meal[k] == v, f"{k} != {v}" assert meal[k] == v, f"{k} != {v}"
@pytest.mark.dependency(depends=["test_create_meal"]) @pytest.mark.anyio
def test_list_presets(client, meal_save_payload): async def test_list_presets(client, meal_save_payload):
response = client.get("preset") response = await client.get("preset")
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
assert len(response.json()["presets"]) > 0, response.json() assert len(response.json()["presets"]) > 0, response.json()
name = meal_save_payload(0)["name"] name = meal_save_payload(0)["name"]
response = client.get(f"preset?q={name}") response = await client.get(f"preset?q={name}")
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
assert len(response.json()["presets"]) > 0, response.json() assert len(response.json()["presets"]) > 0, response.json()
@pytest.mark.dependency(depends=["test_list_presets"]) @pytest.mark.anyio
def test_create_meal_from_preset(client, meal_from_preset): async def test_create_meal_from_preset(client, meal_from_preset):
today = datetime.date.today().isoformat() today = datetime.date.today().isoformat()
response = client.get("diary", params={"date": today}) response = await client.get("diary", params={"date": today})
diary_id = response.json()["id"] diary_id = response.json()["id"]
meal_order = len(response.json()["meals"]) + 1 meal_order = len(response.json()["meals"]) + 1
response = client.get("preset") response = await client.get("preset")
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
assert len(response.json()["presets"]) > 0, response.json() assert len(response.json()["presets"]) > 0, response.json()
@ -77,7 +81,7 @@ def test_create_meal_from_preset(client, meal_from_preset):
preset["id"], preset["id"],
) )
response = client.post("meal/from_preset", json=payload) response = await client.post("meal/from_preset", json=payload)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
meal = response.json() meal = response.json()
@ -88,16 +92,16 @@ def test_create_meal_from_preset(client, meal_from_preset):
assert meal[k] == v, f"{k} != {v}" assert meal[k] == v, f"{k} != {v}"
@pytest.mark.dependency(depends=["test_list_presets"]) @pytest.mark.anyio
def test_delete_preset(client): async def test_delete_preset(client):
presets = client.get("preset").json()["presets"] presets = (await client.get("preset")).json()["presets"]
preset_id = presets[0]["id"] preset_id = presets[0]["id"]
response = client.get(f"preset/{preset_id}") response = await client.get(f"preset/{preset_id}")
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
response = client.delete(f"preset/{preset_id}") response = await client.delete(f"preset/{preset_id}")
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
response = client.get(f"preset/{preset_id}") response = await client.get(f"preset/{preset_id}")
assert response.status_code == 404, response.json() assert response.status_code == 404, response.json()

View file

@ -1,15 +1,15 @@
import pytest import pytest
@pytest.mark.dependency() @pytest.mark.anyio
def test_create_product(client, product_payload): async def test_create_product(client, product_payload):
response = client.post("product", json=product_payload) response = await client.post("product", json=product_payload)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
@pytest.mark.dependency(depends=["test_create_product"]) @pytest.mark.anyio
def test_list_product(client): async def test_list_product(client):
response = client.get("product") response = await client.get("product")
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
data = response.json()["products"] data = response.json()["products"]
@ -21,13 +21,15 @@ def test_list_product(client):
product_ids.add(product["id"]) product_ids.add(product["id"])
@pytest.mark.dependency(depends=["test_create_product"]) @pytest.mark.anyio
def test_get_product_by_barcode(client): async def test_get_product_by_barcode(client):
response = client.get("product/by_barcode", params={"barcode": "4056489666028"}) response = await client.get(
"product/by_barcode", params={"barcode": "4056489666028"}
)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
name = response.json()["name"] name = response.json()["name"]
response = client.get("product", params={"q": name}) response = await client.get("product", params={"q": name})
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
assert len(response.json()["products"]) == 1 assert len(response.json()["products"]) == 1

View file

@ -1,15 +1,15 @@
import pytest import pytest
@pytest.mark.dependency() @pytest.mark.anyio
def test_user_creation(unauthorized_client, user_payload_factory): async def test_user_creation(unauthorized_client, user_payload_factory):
response = unauthorized_client.post("user", json=user_payload_factory()) response = await unauthorized_client.post("user", json=user_payload_factory())
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
@pytest.mark.dependency(depends=["test_user_creation"]) @pytest.mark.anyio
def test_user_login(client, user_payload): async def test_user_login(client, user_payload):
response = client.post("token", data=user_payload) response = await client.post("token", data=user_payload)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
data = response.json() data = response.json()
@ -17,13 +17,13 @@ def test_user_login(client, user_payload):
assert data["refresh_token"] is not None assert data["refresh_token"] is not None
@pytest.mark.dependency(depends=["test_user_login"]) @pytest.mark.anyio
def test_user_refresh_token(client, user_payload): async def test_user_refresh_token(client, user_payload):
response = client.post("token", data=user_payload) response = await client.post("token", data=user_payload)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()
token = response.json()["refresh_token"] token = response.json()["refresh_token"]
payload = {"refresh_token": token} payload = {"refresh_token": token}
response = client.post("token/refresh", json=payload) response = await client.post("token/refresh", json=payload)
assert response.status_code == 200, response.json() assert response.status_code == 200, response.json()

View file

@ -48,5 +48,6 @@ def find(bar_code: str) -> Product:
fiber=data["product"]["nutriments"].get("fiber_100g", 0.0), fiber=data["product"]["nutriments"].get("fiber_100g", 0.0),
) )
except Exception as e: except Exception as e:
raise e
logger.error(e) logger.error(e)
raise ParseError() raise ParseError()

View file

@ -11,3 +11,4 @@ requests
black black
flake8 flake8
flake8-bugbear flake8-bugbear
httpx

60
test.sh
View file

@ -3,60 +3,32 @@
# Run fooder api tests # Run fooder api tests
# #
set -e
TESTS='test'
[[ $# -eq 1 ]] && TESTS=${1}
echo "Running fooder api tests" echo "Running fooder api tests"
# up containers # if exists, remove test.db
DC='docker-compose -f docker-compose.test.yml' [ -f test.db ] && rm test.db
if [ "$(${DC} ps | grep -E 'running|Up' | wc -l)" -gt 0 ]; then
# down containers to assure a clean database
echo "stopping containers..."
${DC} down --remove-orphans
fi
# create test env values # create test env values
export POSTGRES_USER=fooder_test export DB_URI="sqlite+aiosqlite:///test.db"
export POSTGRES_DATABASE=fooder_test export ECHO_SQL=0
export POSTGRES_PASSWORD=$(pwgen 13 1)
export SECRET_KEY=$(openssl rand -hex 32) export SECRET_KEY=$(openssl rand -hex 32)
export REFRESH_SECRET=$(openssl rand -hex 32) export REFRESH_SECRET_KEY=$(openssl rand -hex 32)
rm -f .env.test python -m fooder --create-tables
envsubst < env.template > .env.test
# finally run tests
if [[ $# -eq 1 ]]; then
python -m pytest fooder --disable-warnings -sv -k "${1}"
else
python -m pytest fooder --disable-warnings -sv
fi
# unset test env values
unset POSTGRES_USER unset POSTGRES_USER
unset POSTGRES_DATABASE unset POSTGRES_DATABASE
unset POSTGRES_PASSWORD unset POSTGRES_PASSWORD
unset SECRET_KEY unset SECRET_KEY
unset REFRESH_SECRET unset REFRESH_SECRET
echo "starting containers..." # if exists, remove test.db
${DC} up -d [ -f test.db ] && rm test.db
# Wait for the containers to start
echo "waiting for containers to start..."
while [ "$(${DC} ps | grep -E 'running|Up' | wc -l)" -lt 2 ]; do
sleep 1
done
while [ "$(${DC} logs database | grep -E 'database system is ready to accept connections' | wc -l)" -lt 2 ]; do
sleep 1
done
# create tables
echo "creating tables..."
${DC} exec api bash -c "python -m fooder --create-tables"
# finally run tests
set -xe
python -m pytest fooder -sv -k "${TESTS}"
# clean up after tests
echo "cleaning up..."
${DC} down --remove-orphans
rm -f .env.test