Skip to content
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
11 changes: 9 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,15 @@ jobs:
--health-timeout 5s
--health-retries 5

env:
DB_HOST: localhost # override docker container name from .env
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd 'redis-cli ping'
--health-interval 10s
--health-timeout 5s
--health-retries 5
# =====
# Hardcode will be replaced with secrets GitHub Repo
# =====
Expand Down
8 changes: 7 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dev = [
'pytest>=9.0.2',
'pytest-asyncio>=1.3.0',
'pytest-cov>=7.0.0',
'pytest-env>=1.5.0',
'ruff>=0.15.0',
]

Expand All @@ -56,7 +57,12 @@ python_classes = ['Test*']
python_functions = ['test_*']
pythonpath = ['.']
asyncio_mode = 'auto'
asyncio_default_fixture_loop_scope = 'function'
asyncio_default_fixture_loop_scope = 'session'
asyncio_default_test_loop_scope = 'session'
env = [
'DB_HOST=localhost',
'REDIS_HOST=localhost'
]

[tool.mypy]
python_version = '3.11'
Expand Down
32 changes: 32 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from collections.abc import AsyncGenerator

import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from redis.asyncio import Redis
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
Expand All @@ -15,6 +17,7 @@
import app.services.user.models # noqa: F401
from app.core.config import settings
from app.core.database import Base
from app.main import app as main_app


def _test_db_url() -> str:
Expand Down Expand Up @@ -54,3 +57,32 @@ async def db_session(
) -> AsyncGenerator[AsyncSession, None]:
async with db_session_factory() as session:
yield session


def _test_redis_url() -> str:
host = os.environ.get('REDIS_HOST', 'localhost')
return f'redis://{host}:{settings.redis_port}'


@pytest_asyncio.fixture
async def redis_client() -> AsyncGenerator[Redis, None]:
redis = Redis.from_url(_test_redis_url(), decode_responses=True)
await redis.flushdb()
yield redis
await redis.flushdb()
await redis.aclose()


@pytest_asyncio.fixture
async def async_client(
db_session_factory: async_sessionmaker[AsyncSession], redis_client: Redis
) -> AsyncGenerator[AsyncClient, None]:
from app.core.lua_scripts import RATE_LIMIT_LUA_SCRIPT

main_app.state.redis = redis_client
main_app.state.rate_limit_script = redis_client.register_script(
RATE_LIMIT_LUA_SCRIPT
)
transport = ASGITransport(app=main_app)
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
yield client
46 changes: 46 additions & 0 deletions tests/test_integration_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from http import HTTPStatus
from uuid import uuid4

import pytest
from httpx import AsyncClient


@pytest.mark.asyncio(loop_scope='session')
async def test_auth_flow(async_client: AsyncClient) -> None:
test_email = f'{uuid4().hex[:8]}@example.com'
test_password = 'super_secret_password'
response = await async_client.post(
'/api/v1/users', json={'email': test_email, 'password': test_password}
)
assert response.status_code == HTTPStatus.CREATED
response_data = response.json()
assert response_data['email'] == test_email

response = await async_client.post(
'/api/v1/users', json={'email': test_email, 'password': test_password}
)
assert response.status_code == HTTPStatus.BAD_REQUEST
response_data = response.json()
assert 'already' in response_data['detail']

response = await async_client.post(
'/api/v1/auth/token', data={'username': test_email, 'password': test_password}
)
assert response.status_code == HTTPStatus.OK
response_data = response.json()
access_token = response_data['access_token']
refresh_token = response_data['refresh_token']

response = await async_client.post(
'/api/v1/auth/refresh', json={'refresh_token': refresh_token}
)
assert response.status_code == HTTPStatus.OK
response_data = response.json()
access_token = response_data['access_token']

response = await async_client.get(
'/api/v1/users/me', headers={'Authorization': f'Bearer {access_token}'}
)
assert response.status_code == HTTPStatus.OK
response_data = response.json()
assert response_data['email'] == test_email
57 changes: 57 additions & 0 deletions tests/test_integration_orders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from http import HTTPStatus
from uuid import uuid4

from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from app.services.inventory.models import Product


async def test_reserve_order_flow(
async_client: AsyncClient, db_session: AsyncSession
) -> None:
test_email = f'{uuid4().hex[:8]}@example.com'
test_password = 'super_secret_password'
test_product_id = uuid4()
idempotency_key = uuid4().hex
test_product = Product(
id=test_product_id,
name='Hatchback 02 blue',
description='dayz car such as golf II blue',
price='150',
qty_available=5,
)
db_session.add(test_product)
await db_session.commit()
test_user = await async_client.post(
'/api/v1/users', json={'email': test_email, 'password': test_password}
)
assert test_user.status_code == HTTPStatus.CREATED
test_user_login = await async_client.post(
'/api/v1/auth/token', data={'username': test_email, 'password': test_password}
)
assert test_user_login.status_code == HTTPStatus.OK
test_user_access_token = test_user_login.json()['access_token']
headers = {
'Authorization': f'Bearer {test_user_access_token}',
'X-Idempotency-Key': idempotency_key,
}
test_reserve = await async_client.post(
'/api/v1/inventory/reserve',
json={'product_id': str(test_product_id), 'quantity': 1},
headers=headers,
)
assert test_reserve.status_code == HTTPStatus.OK
test_reserve_data = test_reserve.json()
assert 'id' in test_reserve_data
reservation_id = test_reserve_data['id']
order_idempotency_key = uuid4().hex
headers['X-Idempotency-Key'] = order_idempotency_key
test_order = await async_client.post(
'/api/v1/orders/', json={'reservation_id': reservation_id}, headers=headers
)
assert test_order.status_code == HTTPStatus.OK
test_order_data = test_order.json()
assert test_order_data['status'] == 'pending'
assert test_order_data['total_amount'] == '150.00'
assert 'id' in test_order_data
73 changes: 73 additions & 0 deletions tests/test_integration_reserve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from http import HTTPStatus
from uuid import uuid4

from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession

from app.services.inventory.models import Product


async def test_reserve_flow(
async_client: AsyncClient,
db_session: AsyncSession,
) -> None:
test_email = f'{uuid4().hex[:8]}@example.com'
test_password = 'super_secret_password'
test_product_id = uuid4()
idempotency_key = uuid4().hex
test_product = Product(
id=test_product_id,
name='Hatchback 02 red',
description='dayz car such as golf II',
price='100',
qty_available=5,
)
db_session.add(test_product)
await db_session.commit()
test_user = await async_client.post(
'/api/v1/users', json={'email': test_email, 'password': test_password}
)
assert test_user.status_code == HTTPStatus.CREATED
test_user_login = await async_client.post(
'/api/v1/auth/token', data={'username': test_email, 'password': test_password}
)
assert test_user_login.status_code == HTTPStatus.OK
test_user_access_token = test_user_login.json()['access_token']
headers = {
'Authorization': f'Bearer {test_user_access_token}',
'X-Idempotency-Key': idempotency_key,
}
test_reserve = await async_client.post(
'/api/v1/inventory/reserve',
json={'product_id': str(test_product_id), 'quantity': 1},
headers=headers,
)
assert test_reserve.status_code == HTTPStatus.OK
test_reserve_data = test_reserve.json()
assert 'id' in test_reserve_data
assert test_reserve_data['product_id'] == str(test_product_id)
assert test_reserve_data['status'] == 'pending'

test_reserve = await async_client.post(
'/api/v1/inventory/reserve',
json={'product_id': str(test_product_id), 'quantity': 1},
headers=headers,
)
assert test_reserve.status_code == HTTPStatus.OK
test_reserve_data = test_reserve.json()
assert 'id' in test_reserve_data
assert test_reserve_data['product_id'] == str(test_product_id)

status_codes = []
for _ in range(20):
response = await async_client.post(
'/api/v1/inventory/reserve',
json={'product_id': str(test_product_id), 'quantity': 1},
headers={
'Authorization': f'Bearer {test_user_access_token}',
'X-Idempotency-Key': uuid4().hex,
},
)
status_codes.append(response.status_code)

assert HTTPStatus.TOO_MANY_REQUESTS in status_codes
16 changes: 16 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading