Skip to content

Commit ebeb15a

Browse files
committed
Add session-scoped app running and deprecate aidbox fixture
1 parent 883887b commit ebeb15a

3 files changed

Lines changed: 108 additions & 63 deletions

File tree

aidbox_python_sdk/pytest_plugin.py

Lines changed: 96 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1+
import asyncio
12
import importlib
23
import os
3-
from typing import cast
4+
import threading
5+
import warnings
6+
from collections.abc import Generator
7+
from types import SimpleNamespace
48

59
import pytest
610
from aiohttp import BasicAuth, ClientSession, web
711
from yarl import URL
812

13+
from aidbox_python_sdk.aidboxpy import AsyncAidboxClient
14+
915
from . import app_keys as ak
1016

17+
_TEST_SERVER_URL = "http://127.0.0.1:8081"
18+
1119

1220
def pytest_addoption(parser):
1321
parser.addini(
@@ -34,20 +42,92 @@ def create_app(request):
3442
return _load_create_app(path)
3543

3644

37-
async def start_app(aiohttp_client, create_app):
38-
app = await aiohttp_client(create_app(), server_kwargs={"host": "0.0.0.0", "port": 8081})
39-
sdk = cast(web.Application, app.server.app)[ak.sdk]
45+
@pytest.fixture(scope="session", autouse=True)
46+
def app(create_app) -> Generator[web.Application, None, None]:
47+
"""Start the aiohttp application server in a background thread.
48+
49+
Uses a dedicated event loop running continuously via loop.run_forever()
50+
in a daemon thread. This is necessary (rather than an async fixture) because
51+
the app needs the loop running at all times to handle external callbacks
52+
from Aidbox (subscriptions, SDK heartbeats, etc.), not just during await
53+
points in test code.
54+
55+
The server is guaranteed to be listening before any test runs (no sleep-based
56+
waiting) since site.start() completes synchronously before yielding.
57+
"""
58+
59+
app = create_app()
60+
loop = asyncio.new_event_loop()
61+
62+
runner = web.AppRunner(app)
63+
loop.run_until_complete(runner.setup())
64+
site = web.TCPSite(runner, host="0.0.0.0", port=8081)
65+
loop.run_until_complete(site.start())
66+
67+
thread = threading.Thread(target=loop.run_forever, daemon=True)
68+
thread.start()
69+
70+
yield app
71+
72+
loop.call_soon_threadsafe(loop.stop)
73+
thread.join(timeout=5)
74+
loop.run_until_complete(runner.cleanup())
75+
loop.close()
76+
77+
78+
@pytest.fixture
79+
async def client(app):
80+
server = SimpleNamespace(app=app)
81+
session = ClientSession(base_url=URL(_TEST_SERVER_URL))
82+
wrapper = SimpleNamespace(server=server)
83+
wrapper.get = session.get
84+
wrapper.post = session.post
85+
wrapper.put = session.put
86+
wrapper.patch = session.patch
87+
wrapper.delete = session.delete
88+
wrapper.request = session.request
89+
wrapper._session = session
90+
try:
91+
yield wrapper
92+
finally:
93+
await session.close()
94+
95+
96+
@pytest.fixture
97+
async def safe_db(aidbox_client: AsyncAidboxClient, sdk):
98+
results = await aidbox_client.execute(
99+
"/$psql",
100+
data={"query": "SELECT last_value from transaction_id_seq;"},
101+
)
102+
txid = results[0]["result"][0]["last_value"]
103+
sdk._test_start_txid = int(txid)
104+
105+
yield txid
106+
40107
sdk._test_start_txid = -1
108+
await aidbox_client.execute(
109+
"/$psql",
110+
data={"query": f"select drop_before_all({txid});"},
111+
params={"execute": "true"},
112+
)
113+
114+
115+
@pytest.fixture
116+
def sdk(app):
117+
return app[ak.sdk]
41118

42-
return app
43119

120+
@pytest.fixture
121+
def aidbox_client(app):
122+
return app[ak.client]
44123

45-
@pytest.fixture()
46-
async def client(aiohttp_client, create_app):
47-
"""Instance of app's server and client"""
48-
return await start_app(aiohttp_client, create_app)
49124

125+
@pytest.fixture
126+
def aidbox_db(app):
127+
return app[ak.db]
50128

129+
130+
# Deprecated
51131
class AidboxSession(ClientSession):
52132
def __init__(self, *args, base_url=None, **kwargs):
53133
base_url_resolved = base_url or os.environ.get("AIDBOX_BASE_URL")
@@ -63,51 +143,17 @@ async def _request(self, method, path, *args, **kwargs):
63143
return await super()._request(method, url, *args, **kwargs)
64144

65145

66-
@pytest.fixture()
67-
async def aidbox(client):
68-
"""HTTP client for making requests to Aidbox"""
69-
app = cast(web.Application, client.server.app)
146+
@pytest.fixture
147+
async def aidbox(sdk, app):
148+
warnings.warn(
149+
"The 'aidbox' fixture is deprecated; use 'aidbox_client' for the Aidbox client instead.",
150+
DeprecationWarning,
151+
stacklevel=2,
152+
)
70153
basic_auth = BasicAuth(
71154
login=app[ak.settings].APP_INIT_CLIENT_ID,
72155
password=app[ak.settings].APP_INIT_CLIENT_SECRET,
73156
)
74157
session = AidboxSession(auth=basic_auth, base_url=app[ak.settings].APP_INIT_URL)
75158
yield session
76159
await session.close()
77-
78-
79-
@pytest.fixture()
80-
async def safe_db(aidbox, client, sdk):
81-
resp = await aidbox.post(
82-
"/$psql",
83-
json={"query": "SELECT last_value from transaction_id_seq;"},
84-
raise_for_status=True,
85-
)
86-
results = await resp.json()
87-
txid = results[0]["result"][0]["last_value"]
88-
sdk._test_start_txid = int(txid)
89-
90-
yield txid
91-
92-
sdk._test_start_txid = -1
93-
await aidbox.post(
94-
"/$psql",
95-
json={"query": f"select drop_before_all({txid});"},
96-
params={"execute": "true"},
97-
raise_for_status=True,
98-
)
99-
100-
101-
@pytest.fixture()
102-
def sdk(client):
103-
return cast(web.Application, client.server.app)[ak.sdk]
104-
105-
106-
@pytest.fixture()
107-
def aidbox_client(client):
108-
return cast(web.Application, client.server.app)[ak.client]
109-
110-
111-
@pytest.fixture()
112-
def aidbox_db(client):
113-
return cast(web.Application, client.server.app)[ak.db]

aidbox_python_sdk/sdk.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717

1818
class SDK:
19-
def __init__( # noqa: PLR0913
19+
def __init__(
2020
self,
2121
settings,
2222
*,

tests/test_sdk.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
@pytest.mark.skip("Skipped because of regression in Aidbox 2510")
12-
@pytest.mark.asyncio()
12+
@pytest.mark.asyncio
1313
@pytest.mark.parametrize(
1414
("expression", "expected"),
1515
[
@@ -36,24 +36,24 @@ async def test_operation_with_compliance_params(aidbox_client, expression, expec
3636
assert evaluate(response, expression, {})[0] == expected
3737

3838

39-
@pytest.mark.asyncio()
39+
@pytest.mark.asyncio
4040
async def test_health_check(client):
4141
resp = await client.get("/health")
4242
assert resp.status == 200
4343
json = await resp.json()
4444
assert json == {"status": "OK"}
4545

4646

47-
@pytest.mark.asyncio()
47+
@pytest.mark.asyncio
4848
async def test_live_health_check(client):
4949
resp = await client.get("/live")
5050
assert resp.status == 200
5151
json = await resp.json()
5252
assert json == {"status": "OK"}
5353

5454

55-
@pytest.mark.skip()
56-
async def test_signup_reg_op(client, aidbox):
55+
@pytest.mark.skip
56+
async def test_signup_reg_op(aidbox):
5757
resp = await aidbox.post("signup/register/21.02.19/testvalue")
5858
assert resp.status == 200
5959
json = await resp.json()
@@ -63,13 +63,12 @@ async def test_signup_reg_op(client, aidbox):
6363
}
6464

6565

66-
@pytest.mark.skip()
67-
async def test_appointment_sub(client, aidbox):
66+
@pytest.mark.skip
67+
async def test_appointment_sub(sdk, aidbox):
6868
with mock.patch.object(main, "_appointment_sub") as appointment_sub:
6969
f = asyncio.Future()
7070
f.set_result("")
7171
appointment_sub.return_value = f
72-
sdk = client.server.app["sdk"]
7372
was_appointment_sub_triggered = sdk.was_subscription_triggered("Appointment")
7473
resp = await aidbox.post(
7574
"Appointment",
@@ -94,7 +93,7 @@ async def test_appointment_sub(client, aidbox):
9493
assert expected["resource"].items() <= event["resource"].items()
9594

9695

97-
@pytest.mark.asyncio()
96+
@pytest.mark.asyncio
9897
async def test_database_isolation__1(aidbox_client, safe_db):
9998
patients = await aidbox_client.resources("Patient").fetch_all()
10099
assert len(patients) == 2
@@ -106,7 +105,7 @@ async def test_database_isolation__1(aidbox_client, safe_db):
106105
assert len(patients) == 3
107106

108107

109-
@pytest.mark.asyncio()
108+
@pytest.mark.asyncio
110109
async def test_database_isolation__2(aidbox_client, safe_db):
111110
patients = await aidbox_client.resources("Patient").fetch_all()
112111
assert len(patients) == 2
@@ -121,7 +120,7 @@ async def test_database_isolation__2(aidbox_client, safe_db):
121120
assert len(patients) == 4
122121

123122

124-
@pytest.mark.asyncio()
123+
@pytest.mark.asyncio
125124
async def test_database_isolation_with_history_in_name__1(aidbox_client, safe_db):
126125
resources = await aidbox_client.resources("FamilyMemberHistory").fetch_all()
127126
assert len(resources) == 0
@@ -144,7 +143,7 @@ async def test_database_isolation_with_history_in_name__1(aidbox_client, safe_db
144143
assert len(resources) == 1
145144

146145

147-
@pytest.mark.asyncio()
146+
@pytest.mark.asyncio
148147
async def test_database_isolation_with_history_in_name__2(aidbox_client, safe_db):
149148
resources = await aidbox_client.resources("FamilyMemberHistory").fetch_all()
150149
assert len(resources) == 0

0 commit comments

Comments
 (0)