From 4b7075f91a84b4d802d1713ab87eb5fa2518b348 Mon Sep 17 00:00:00 2001 From: Sreekar Nutulapati Date: Thu, 16 Apr 2026 12:55:23 +0530 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=85=20implement=20tests=20for=20walle?= =?UTF-8?q?t=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/wallet/pyproject.toml | 12 + apps/wallet/tests/__init__.py | 0 apps/wallet/tests/conftest.py | 59 ++++ apps/wallet/tests/helpers.py | 22 ++ apps/wallet/tests/test_auth_events.py | 54 ++++ apps/wallet/tests/test_servicer.py | 290 ++++++++++++++++++ apps/wallet/tests/test_wallet_admin_routes.py | 85 +++++ apps/wallet/tests/test_wallet_user_routes.py | 102 ++++++ uv.lock | 58 ++++ 9 files changed, 682 insertions(+) create mode 100644 apps/wallet/tests/__init__.py create mode 100644 apps/wallet/tests/conftest.py create mode 100644 apps/wallet/tests/helpers.py create mode 100644 apps/wallet/tests/test_auth_events.py create mode 100644 apps/wallet/tests/test_servicer.py create mode 100644 apps/wallet/tests/test_wallet_admin_routes.py create mode 100644 apps/wallet/tests/test_wallet_user_routes.py diff --git a/apps/wallet/pyproject.toml b/apps/wallet/pyproject.toml index 64f6a4e..6826b2b 100644 --- a/apps/wallet/pyproject.toml +++ b/apps/wallet/pyproject.toml @@ -40,3 +40,15 @@ sources = ["src"] [tool.uv.sources] mint-shared = { workspace = true } + +[project.optional-dependencies] +test = [ + "pytest>=8.0.0", + "pytest-asyncio>=0.24.0", + "httpx>=0.27.0", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +pythonpath = ["src", "tests"] diff --git a/apps/wallet/tests/__init__.py b/apps/wallet/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/wallet/tests/conftest.py b/apps/wallet/tests/conftest.py new file mode 100644 index 0000000..372a26b --- /dev/null +++ b/apps/wallet/tests/conftest.py @@ -0,0 +1,59 @@ +import os +import pytest + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from wallet.core.deps import require_auth +from wallet.db import get_db +from wallet.routes.wallet_admin import wallet_admin_route +from wallet.routes.wallet_user import wallet_user_route + + +from helpers import make_session_mock + + +os.environ.setdefault("APP_PORT", "4002") +os.environ.setdefault("APP_HOST", "localhost") +os.environ.setdefault("APP_RELOAD", "False") +os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") +os.environ.setdefault("KAFKA_BROKERS", "localhost:9092") + + +@pytest.fixture +def session(): + return make_session_mock() + + +@pytest.fixture +def user_client(session): + app = FastAPI() + app.include_router(wallet_user_route, prefix="/api/v1/wallet") + + async def override_auth(): + return {"sub": "u-1", "roles": ["user"]} + + async def override_db(): + yield session + + app.dependency_overrides[require_auth] = override_auth + app.dependency_overrides[get_db] = override_db + + return TestClient(app) + + +@pytest.fixture +def admin_client(session): + app = FastAPI() + app.include_router(wallet_admin_route, prefix="/api/v1/wallet") + + async def override_auth(): + return {"sub": "admin-1", "roles": ["admin"]} + + async def override_db(): + yield session + + app.dependency_overrides[require_auth] = override_auth + app.dependency_overrides[get_db] = override_db + + return TestClient(app) diff --git a/apps/wallet/tests/helpers.py b/apps/wallet/tests/helpers.py new file mode 100644 index 0000000..72694d4 --- /dev/null +++ b/apps/wallet/tests/helpers.py @@ -0,0 +1,22 @@ +from unittest.mock import AsyncMock, MagicMock + + +def make_session_mock(): + session = MagicMock() + session.execute = AsyncMock() + session.flush = AsyncMock() + session.commit = AsyncMock() + session.add = MagicMock() + + cm = MagicMock() + cm.__aenter__ = AsyncMock(return_value=session) + cm.__aexit__ = AsyncMock(return_value=False) + session.begin.return_value = cm + return session + + +def make_get_db(session): + async def _get_db(): + yield session + + return _get_db diff --git a/apps/wallet/tests/test_auth_events.py b/apps/wallet/tests/test_auth_events.py new file mode 100644 index 0000000..4c1a3b8 --- /dev/null +++ b/apps/wallet/tests/test_auth_events.py @@ -0,0 +1,54 @@ +import os +import pytest + +from unittest.mock import AsyncMock, MagicMock +from wallet.kafka.consumers.auth_events import handle_auth_events +from wallet.kafka.consumers.schema import KafkaEnvelope +from wallet.models.wallet import Wallet + +os.environ.setdefault("APP_PORT", "4002") +os.environ.setdefault("APP_HOST", "localhost") +os.environ.setdefault("APP_RELOAD", "False") +os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") +os.environ.setdefault("KAFKA_BROKERS", "localhost:9092") + + +def make_envelope(event: str, payload_extra: dict = {}) -> KafkaEnvelope: + return KafkaEnvelope( + topic="auth.events", + eventId="evt-1", + version="1", + actorId="u-1", + payload={"event": event, **payload_extra}, + ) + + +@pytest.fixture +def db_session(): + session = AsyncMock() + session.commit = AsyncMock() + session.add = MagicMock() + return session + + +class TestHandleAuthEvents: + @pytest.mark.asyncio + async def test_user_registered_creates_wallet(self, db_session): + envelope = make_envelope("auth.user_registered", {"userId": "u-42"}) + + await handle_auth_events(envelope, db_session) + + db_session.add.assert_called_once() + added = db_session.add.call_args[0][0] + assert isinstance(added, Wallet) + assert added.user_id == "u-42" + db_session.commit.assert_called_once() + + @pytest.mark.asyncio + async def test_unknown_event_does_nothing(self, db_session): + envelope = make_envelope("auth.some_other_event", {"userId": "u-42"}) + + await handle_auth_events(envelope, db_session) + + db_session.add.assert_not_called() + db_session.commit.assert_not_called() diff --git a/apps/wallet/tests/test_servicer.py b/apps/wallet/tests/test_servicer.py new file mode 100644 index 0000000..62c5614 --- /dev/null +++ b/apps/wallet/tests/test_servicer.py @@ -0,0 +1,290 @@ +import os +import grpc +import pytest + +from unittest.mock import AsyncMock, MagicMock + +from wallet.grpc.servicer import WalletServicer +from wallet.models.balance_history import BalanceHistory +from wallet.models.wallet import Wallet, WalletStatus + +from helpers import make_get_db, make_session_mock + + +os.environ.setdefault("APP_PORT", "4002") +os.environ.setdefault("APP_HOST", "localhost") +os.environ.setdefault("APP_RELOAD", "False") +os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") +os.environ.setdefault("KAFKA_BROKERS", "localhost:9092") + + +def make_wallet( + id="w-1", + user_id="u-1", + balance=10000, + currency="USD", + status=WalletStatus.ACTIVE, +): + w = MagicMock(spec=Wallet) + w.id = id + w.user_id = user_id + w.balance = balance + w.currency = currency + w.status = status + return w + + +def make_history(wallet_id="w-1", transaction_id="tx-1", balance_after=9000): + h = MagicMock(spec=BalanceHistory) + h.wallet_id = wallet_id + h.transaction_id = transaction_id + h.balance_after = balance_after + return h + + +def make_request(**kwargs): + req = MagicMock() + req.wallet_id = kwargs.get("wallet_id", "w-1") + req.user_id = kwargs.get("user_id", "u-1") + req.transaction_id = kwargs.get("transaction_id", "tx-1") + req.amount_cents = kwargs.get("amount_cents", 1000) + return req + + +@pytest.fixture +def context(): + ctx = AsyncMock() + return ctx + + +@pytest.fixture +def session(): + return make_session_mock() + + +@pytest.fixture +def servicer(session): + return WalletServicer(make_get_db(session)) + + +def setup_execute(session, *scalar_values): + results = [] + for val in scalar_values: + r = MagicMock() + r.scalar_one_or_none.return_value = val + results.append(r) + session.execute = AsyncMock(side_effect=results) + + +class TestDebitWallet: + @pytest.mark.asyncio + async def test_happy_path(self, servicer, session, context): + wallet = make_wallet(balance=10000) + setup_execute(session, wallet, None) + + req = make_request(amount_cents=1000) + resp = await servicer.DebitWallet(req, context) + + assert resp.success is True + assert resp.balance_after == 9000 + context.abort.assert_not_called() + + @pytest.mark.asyncio + async def test_wallet_not_found(self, servicer, session, context): + setup_execute(session, None) + + req = make_request() + resp = await servicer.DebitWallet(req, context) + + context.abort.assert_called_once() + assert context.abort.call_args[0][0] == grpc.StatusCode.NOT_FOUND + assert resp.success is False + + @pytest.mark.asyncio + async def test_frozen_wallet(self, servicer, session, context): + wallet = make_wallet(status=WalletStatus.FROZEN) + setup_execute(session, wallet) + + req = make_request() + resp = await servicer.DebitWallet(req, context) + + context.abort.assert_called_once() + assert context.abort.call_args[0][0] == grpc.StatusCode.FAILED_PRECONDITION + assert resp.success is False + + @pytest.mark.asyncio + async def test_insufficient_funds(self, servicer, session, context): + wallet = make_wallet(balance=500) + setup_execute(session, wallet, None) + + req = make_request(amount_cents=1000) + resp = await servicer.DebitWallet(req, context) + + context.abort.assert_called_once() + assert context.abort.call_args[0][0] == grpc.StatusCode.FAILED_PRECONDITION + assert "Insufficient" in resp.error + + @pytest.mark.asyncio + async def test_idempotent_debit(self, servicer, session, context): + wallet = make_wallet(balance=10000) + existing = make_history(balance_after=9000) + setup_execute(session, wallet, existing) + + req = make_request(amount_cents=1000) + resp = await servicer.DebitWallet(req, context) + + assert resp.success is True + assert resp.balance_after == 9000 + session.add.assert_not_called() + + +class TestCreditWallet: + @pytest.mark.asyncio + async def test_happy_path(self, servicer, session, context): + wallet = make_wallet(balance=5000) + setup_execute(session, wallet, None) + + req = make_request(amount_cents=2000) + resp = await servicer.CreditWallet(req, context) + + assert resp.success is True + assert resp.balance_after == 7000 + context.abort.assert_not_called() + + @pytest.mark.asyncio + async def test_wallet_not_found(self, servicer, session, context): + setup_execute(session, None) + + req = make_request() + resp = await servicer.CreditWallet(req, context) + + context.abort.assert_called_once() + assert context.abort.call_args[0][0] == grpc.StatusCode.NOT_FOUND + assert resp.success is False + + @pytest.mark.asyncio + async def test_frozen_wallet(self, servicer, session, context): + wallet = make_wallet(status=WalletStatus.FROZEN) + setup_execute(session, wallet) + + req = make_request() + resp = await servicer.CreditWallet(req, context) + + context.abort.assert_called_once() + assert context.abort.call_args[0][0] == grpc.StatusCode.FAILED_PRECONDITION + + @pytest.mark.asyncio + async def test_idempotent_credit(self, servicer, session, context): + wallet = make_wallet(balance=5000) + existing = make_history(balance_after=7000) + setup_execute(session, wallet, existing) + + req = make_request(amount_cents=2000) + resp = await servicer.CreditWallet(req, context) + + assert resp.success is True + assert resp.balance_after == 7000 + session.add.assert_not_called() + + +class TestGetBalance: + @pytest.mark.asyncio + async def test_returns_balance(self, servicer, session, context): + wallet = make_wallet(balance=12345, currency="EUR") + setup_execute(session, wallet) + + req = make_request() + resp = await servicer.GetBalance(req, context) + + assert resp.balance == 12345 + assert resp.currency == "EUR" + + @pytest.mark.asyncio + async def test_wallet_not_found(self, servicer, session, context): + setup_execute(session, None) + + req = make_request() + await servicer.GetBalance(req, context) + + context.abort.assert_called_once() + assert context.abort.call_args[0][0] == grpc.StatusCode.NOT_FOUND + + +class TestFreezeWallet: + @pytest.mark.asyncio + async def test_freezes_wallet(self, servicer, session, context): + result = MagicMock() + result.rowcount = 1 + session.execute = AsyncMock(return_value=result) + + req = make_request() + resp = await servicer.FreezeWallet(req, context) + + assert resp.success is True + assert "frozen" in resp.message.lower() + + @pytest.mark.asyncio + async def test_wallet_not_found(self, servicer, session, context): + result = MagicMock() + result.rowcount = 0 + session.execute = AsyncMock(return_value=result) + + req = make_request() + resp = await servicer.FreezeWallet(req, context) + + context.abort.assert_called_once() + assert context.abort.call_args[0][0] == grpc.StatusCode.NOT_FOUND + assert resp.success is False + + +class TestUnfreezeWallet: + @pytest.mark.asyncio + async def test_unfreezes_wallet(self, servicer, session, context): + result = MagicMock() + result.rowcount = 1 + session.execute = AsyncMock(return_value=result) + + req = make_request() + resp = await servicer.UnfreezeWallet(req, context) + + assert resp.success is True + assert "unfrozen" in resp.message.lower() + + @pytest.mark.asyncio + async def test_wallet_not_found(self, servicer, session, context): + result = MagicMock() + result.rowcount = 0 + session.execute = AsyncMock(return_value=result) + + req = make_request() + resp = await servicer.UnfreezeWallet(req, context) + + context.abort.assert_called_once() + assert context.abort.call_args[0][0] == grpc.StatusCode.NOT_FOUND + + +class TestGetWallet: + @pytest.mark.asyncio + async def test_returns_wallet(self, servicer, session, context): + wallet = make_wallet(id="w-1", user_id="u-1", balance=5000, currency="USD") + wallet.is_default = True + setup_execute(session, wallet) + + req = make_request(user_id="u-1") + resp = await servicer.GetWallet(req, context) + + assert resp.id == "w-1" + assert resp.user_id == "u-1" + assert resp.balance == 5000 + assert resp.currency == "USD" + assert resp.is_default is True + + @pytest.mark.asyncio + async def test_wallet_not_found(self, servicer, session, context): + setup_execute(session, None) + + req = make_request(user_id="u-99") + await servicer.GetWallet(req, context) + + context.abort.assert_called_once() + assert context.abort.call_args[0][0] == grpc.StatusCode.NOT_FOUND diff --git a/apps/wallet/tests/test_wallet_admin_routes.py b/apps/wallet/tests/test_wallet_admin_routes.py new file mode 100644 index 0000000..7418e43 --- /dev/null +++ b/apps/wallet/tests/test_wallet_admin_routes.py @@ -0,0 +1,85 @@ +import os + +from unittest.mock import AsyncMock, MagicMock +from wallet.models.wallet import Wallet, WalletStatus + + +os.environ.setdefault("APP_PORT", "4002") +os.environ.setdefault("APP_HOST", "localhost") +os.environ.setdefault("APP_RELOAD", "False") +os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") +os.environ.setdefault("KAFKA_BROKERS", "localhost:9092") + + +def make_wallet(id="w-1", user_id="u-1", status=WalletStatus.ACTIVE): + w = MagicMock(spec=Wallet) + w.id = id + w.user_id = user_id + w.status = status + return w + + +def setup_execute(session, scalar_value): + result = MagicMock() + result.scalar_one_or_none.return_value = scalar_value + session.execute = AsyncMock(return_value=result) + + +class TestFreezeWallet: + def test_freezes_active_wallet(self, admin_client, session): + wallet = make_wallet(status=WalletStatus.ACTIVE) + setup_execute(session, wallet) + session.commit = AsyncMock() + + resp = admin_client.post("/api/v1/wallet/freeze?user_id=u-1") + + assert resp.status_code == 200 + assert resp.json()["walletId"] == "w-1" + assert wallet.status == WalletStatus.FROZEN + + def test_wallet_not_found(self, admin_client, session): + setup_execute(session, None) + + resp = admin_client.post("/api/v1/wallet/freeze?user_id=u-99") + + assert resp.status_code == 404 + assert resp.json()["detail"] == "Wallet not found" + + def test_already_frozen(self, admin_client, session): + wallet = make_wallet(status=WalletStatus.FROZEN) + setup_execute(session, wallet) + + resp = admin_client.post("/api/v1/wallet/freeze?user_id=u-1") + + assert resp.status_code == 400 + assert resp.json()["detail"] == "Already frozen" + + +class TestUnfreezeWallet: + def test_unfreezes_frozen_wallet(self, admin_client, session): + wallet = make_wallet(status=WalletStatus.FROZEN) + setup_execute(session, wallet) + session.commit = AsyncMock() + + resp = admin_client.post("/api/v1/wallet/unfreeze?user_id=u-1") + + assert resp.status_code == 200 + assert resp.json()["walletId"] == "w-1" + assert wallet.status == WalletStatus.ACTIVE + + def test_wallet_not_found(self, admin_client, session): + setup_execute(session, None) + + resp = admin_client.post("/api/v1/wallet/unfreeze?user_id=u-99") + + assert resp.status_code == 404 + assert resp.json()["detail"] == "Wallet not found" + + def test_not_frozen(self, admin_client, session): + wallet = make_wallet(status=WalletStatus.ACTIVE) + setup_execute(session, wallet) + + resp = admin_client.post("/api/v1/wallet/unfreeze?user_id=u-1") + + assert resp.status_code == 400 + assert resp.json()["detail"] == "Wallet is not frozen" diff --git a/apps/wallet/tests/test_wallet_user_routes.py b/apps/wallet/tests/test_wallet_user_routes.py new file mode 100644 index 0000000..e19144d --- /dev/null +++ b/apps/wallet/tests/test_wallet_user_routes.py @@ -0,0 +1,102 @@ +import os + + +from datetime import datetime +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock + +from wallet.models.balance_history import BalanceHistory +from wallet.models.wallet import WalletStatus + + +os.environ.setdefault("APP_PORT", "4002") +os.environ.setdefault("APP_HOST", "localhost") +os.environ.setdefault("APP_RELOAD", "False") +os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") +os.environ.setdefault("KAFKA_BROKERS", "localhost:9092") + + +def make_wallet(id="w-1", user_id="u-1", balance=10000, currency="USD"): + return SimpleNamespace( + id=id, + user_id=user_id, + balance=balance, + currency=currency, + status=WalletStatus.ACTIVE, + ) + + +def make_history_entry(id="h-1", delta=-500, balance_after=9500, transaction_id="tx-1"): + h = MagicMock(spec=BalanceHistory) + h.id = id + h.delta = delta + h.balance_after = balance_after + h.transaction_id = transaction_id + h.created_at = datetime(2025, 1, 1, 0, 0, 0) + return h + + +def setup_execute(session, *scalar_values): + results = [] + for val in scalar_values: + if isinstance(val, list): + r = MagicMock() + r.scalars.return_value.all.return_value = val + results.append(r) + else: + r = MagicMock() + r.scalar_one_or_none.return_value = val + results.append(r) + session.execute = AsyncMock(side_effect=results) + + +class TestGetWallet: + def test_returns_wallet(self, user_client, session): + wallet = make_wallet() + setup_execute(session, wallet) + + resp = user_client.get("/api/v1/wallet/") + + assert resp.status_code == 200 + + def test_wallet_not_found(self, user_client, session): + setup_execute(session, None) + + resp = user_client.get("/api/v1/wallet/") + + assert resp.status_code == 404 + assert resp.json()["detail"] == "Wallet not found" + + +class TestGetWalletHistory: + def test_returns_history(self, user_client, session): + wallet = make_wallet() + history = [make_history_entry()] + setup_execute(session, wallet, history) + + resp = user_client.get("/api/v1/wallet/history") + + assert resp.status_code == 200 + body = resp.json() + assert body["walletId"] == "w-1" + assert len(body["history"]) == 1 + assert body["history"][0]["delta"] == -500 + assert body["history"][0]["balanceAfter"] == 9500 + assert body["history"][0]["transactionId"] == "tx-1" + + def test_returns_empty_history(self, user_client, session): + wallet = make_wallet() + setup_execute(session, wallet, []) + + resp = user_client.get("/api/v1/wallet/history") + + assert resp.status_code == 200 + assert resp.json()["history"] == [] + + def test_wallet_not_found(self, user_client, session): + setup_execute(session, None) + + resp = user_client.get("/api/v1/wallet/history") + + assert resp.status_code == 404 + assert resp.json()["detail"] == "Wallet not found" diff --git a/uv.lock b/uv.lock index 6e297b7..bebf698 100644 --- a/uv.lock +++ b/uv.lock @@ -998,6 +998,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -1258,6 +1267,13 @@ dependencies = [ { name = "sqlalchemy", extra = ["asyncio"] }, ] +[package.optional-dependencies] +test = [ + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, +] + [package.metadata] requires-dist = [ { name = "alembic", specifier = ">=1.18.4" }, @@ -1266,6 +1282,7 @@ requires-dist = [ { name = "fastapi", extras = ["all"], specifier = ">=0.135.1" }, { name = "faststream", extras = ["kafka"], specifier = ">=0.6.7" }, { name = "grpcio-tools", specifier = ">=1.80.0" }, + { name = "httpx", marker = "extra == 'test'", specifier = ">=0.27.0" }, { name = "joserfc", specifier = ">=1.6.3" }, { name = "mint-shared", editable = "shared-py" }, { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.25.0" }, @@ -1275,8 +1292,11 @@ requires-dist = [ { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.46b0" }, { name = "opentelemetry-sdk", specifier = ">=1.25.0" }, { name = "pydantic-settings", specifier = ">=2.12.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.24.0" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.48" }, ] +provides-extras = ["test"] [[package]] name = "mkdocs" @@ -1568,6 +1588,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "protobuf" version = "6.33.6" @@ -1758,6 +1787,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, ] +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" From 40e2e80a2f328a657d90c0d8ff5ff500c749ed5d Mon Sep 17 00:00:00 2001 From: Sreekar Nutulapati Date: Thu, 16 Apr 2026 12:59:28 +0530 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=91=B7=20add=20pytest=20job=20for=20g?= =?UTF-8?q?ithub=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pytest.yml | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/pytest.yml diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..066367f --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,42 @@ +name: Pytest + +on: + push: + branches: [main] + paths: + - 'apps/**' + - 'shared-py/**' + - 'pyproject.toml' + - 'uv.lock' + pull_request: + branches: [main] + paths: + - 'apps/**' + - 'shared-py/**' + - 'pyproject.toml' + - 'uv.lock' + +jobs: + test: + name: ${{ matrix.service }} Tests + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - service: wallet + path: apps/wallet + + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --project ${{ matrix.path }} --extra test + + - name: Run tests + run: uv run --project ${{ matrix.path }} pytest ${{ matrix.path }}/tests/ -v From 5a1d1788e2158512fe3c73436002a7af6d7cf7e8 Mon Sep 17 00:00:00 2001 From: Sreekar Nutulapati Date: Thu, 16 Apr 2026 13:05:25 +0530 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=94=A5=20remove=20default=20env=20fro?= =?UTF-8?q?m=20py=20files=20and=20add=20them=20in=20pytest.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pytest.yml | 6 ++++++ apps/wallet/tests/conftest.py | 9 --------- apps/wallet/tests/test_auth_events.py | 7 ------- apps/wallet/tests/test_servicer.py | 8 -------- apps/wallet/tests/test_wallet_admin_routes.py | 9 --------- apps/wallet/tests/test_wallet_user_routes.py | 10 ---------- 6 files changed, 6 insertions(+), 43 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 066367f..ce1acc3 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -40,3 +40,9 @@ jobs: - name: Run tests run: uv run --project ${{ matrix.path }} pytest ${{ matrix.path }}/tests/ -v + env: + APP_PORT: "4002" + APP_HOST: "localhost" + APP_RELOAD: "False" + DATABASE_URL: "postgresql+asyncpg://test:test@localhost/test" + KAFKA_BROKERS: "localhost:9092" diff --git a/apps/wallet/tests/conftest.py b/apps/wallet/tests/conftest.py index 372a26b..969e3fa 100644 --- a/apps/wallet/tests/conftest.py +++ b/apps/wallet/tests/conftest.py @@ -1,4 +1,3 @@ -import os import pytest from fastapi import FastAPI @@ -9,17 +8,9 @@ from wallet.routes.wallet_admin import wallet_admin_route from wallet.routes.wallet_user import wallet_user_route - from helpers import make_session_mock -os.environ.setdefault("APP_PORT", "4002") -os.environ.setdefault("APP_HOST", "localhost") -os.environ.setdefault("APP_RELOAD", "False") -os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") -os.environ.setdefault("KAFKA_BROKERS", "localhost:9092") - - @pytest.fixture def session(): return make_session_mock() diff --git a/apps/wallet/tests/test_auth_events.py b/apps/wallet/tests/test_auth_events.py index 4c1a3b8..5dc6b87 100644 --- a/apps/wallet/tests/test_auth_events.py +++ b/apps/wallet/tests/test_auth_events.py @@ -1,4 +1,3 @@ -import os import pytest from unittest.mock import AsyncMock, MagicMock @@ -6,12 +5,6 @@ from wallet.kafka.consumers.schema import KafkaEnvelope from wallet.models.wallet import Wallet -os.environ.setdefault("APP_PORT", "4002") -os.environ.setdefault("APP_HOST", "localhost") -os.environ.setdefault("APP_RELOAD", "False") -os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") -os.environ.setdefault("KAFKA_BROKERS", "localhost:9092") - def make_envelope(event: str, payload_extra: dict = {}) -> KafkaEnvelope: return KafkaEnvelope( diff --git a/apps/wallet/tests/test_servicer.py b/apps/wallet/tests/test_servicer.py index 62c5614..b107da5 100644 --- a/apps/wallet/tests/test_servicer.py +++ b/apps/wallet/tests/test_servicer.py @@ -1,4 +1,3 @@ -import os import grpc import pytest @@ -11,13 +10,6 @@ from helpers import make_get_db, make_session_mock -os.environ.setdefault("APP_PORT", "4002") -os.environ.setdefault("APP_HOST", "localhost") -os.environ.setdefault("APP_RELOAD", "False") -os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") -os.environ.setdefault("KAFKA_BROKERS", "localhost:9092") - - def make_wallet( id="w-1", user_id="u-1", diff --git a/apps/wallet/tests/test_wallet_admin_routes.py b/apps/wallet/tests/test_wallet_admin_routes.py index 7418e43..0b56f87 100644 --- a/apps/wallet/tests/test_wallet_admin_routes.py +++ b/apps/wallet/tests/test_wallet_admin_routes.py @@ -1,16 +1,7 @@ -import os - from unittest.mock import AsyncMock, MagicMock from wallet.models.wallet import Wallet, WalletStatus -os.environ.setdefault("APP_PORT", "4002") -os.environ.setdefault("APP_HOST", "localhost") -os.environ.setdefault("APP_RELOAD", "False") -os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") -os.environ.setdefault("KAFKA_BROKERS", "localhost:9092") - - def make_wallet(id="w-1", user_id="u-1", status=WalletStatus.ACTIVE): w = MagicMock(spec=Wallet) w.id = id diff --git a/apps/wallet/tests/test_wallet_user_routes.py b/apps/wallet/tests/test_wallet_user_routes.py index e19144d..6bc077e 100644 --- a/apps/wallet/tests/test_wallet_user_routes.py +++ b/apps/wallet/tests/test_wallet_user_routes.py @@ -1,6 +1,3 @@ -import os - - from datetime import datetime from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock @@ -9,13 +6,6 @@ from wallet.models.wallet import WalletStatus -os.environ.setdefault("APP_PORT", "4002") -os.environ.setdefault("APP_HOST", "localhost") -os.environ.setdefault("APP_RELOAD", "False") -os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") -os.environ.setdefault("KAFKA_BROKERS", "localhost:9092") - - def make_wallet(id="w-1", user_id="u-1", balance=10000, currency="USD"): return SimpleNamespace( id=id, From 730ea74ff1e029cacc3b162c5878fc497f8fcec0 Mon Sep 17 00:00:00 2001 From: Sreekar Nutulapati Date: Thu, 16 Apr 2026 13:08:07 +0530 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=92=9A=20add=20proto=20generate=20cmd?= =?UTF-8?q?=20in=20pytest.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pytest.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ce1acc3..953bfd9 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -27,6 +27,7 @@ jobs: include: - service: wallet path: apps/wallet + proto_src: libs/proto/wallet.proto steps: - uses: actions/checkout@v4 @@ -38,6 +39,10 @@ jobs: - name: Install dependencies run: uv sync --project ${{ matrix.path }} --extra test + - name: Generate proto files + if: matrix.proto_src != '' + run: uv run --project ${{ matrix.path }} python scripts/generate_proto.py + - name: Run tests run: uv run --project ${{ matrix.path }} pytest ${{ matrix.path }}/tests/ -v env: