belgie-alchemy 0.1.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- belgie_alchemy/__init__.py +33 -0
- belgie_alchemy/__tests__/__init__.py +0 -0
- belgie_alchemy/__tests__/adapter/__init__.py +0 -0
- belgie_alchemy/__tests__/adapter/test_adapter.py +493 -0
- belgie_alchemy/__tests__/auth_models/__init__.py +0 -0
- belgie_alchemy/__tests__/auth_models/test_auth_models.py +91 -0
- belgie_alchemy/__tests__/base/__init__.py +0 -0
- belgie_alchemy/__tests__/base/test_base.py +90 -0
- belgie_alchemy/__tests__/conftest.py +39 -0
- belgie_alchemy/__tests__/fixtures/__init__.py +12 -0
- belgie_alchemy/__tests__/fixtures/database.py +38 -0
- belgie_alchemy/__tests__/fixtures/models.py +119 -0
- belgie_alchemy/__tests__/mixins/__init__.py +0 -0
- belgie_alchemy/__tests__/mixins/test_mixins.py +80 -0
- belgie_alchemy/__tests__/settings/__init__.py +0 -0
- belgie_alchemy/__tests__/settings/test_settings.py +342 -0
- belgie_alchemy/__tests__/settings/test_settings_integration.py +416 -0
- belgie_alchemy/__tests__/types/__init__.py +0 -0
- belgie_alchemy/__tests__/types/test_types.py +155 -0
- belgie_alchemy/adapter.py +323 -0
- belgie_alchemy/base.py +25 -0
- belgie_alchemy/mixins.py +83 -0
- belgie_alchemy/py.typed +0 -0
- belgie_alchemy/settings.py +146 -0
- belgie_alchemy/types.py +32 -0
- belgie_alchemy-0.1.0a4.dist-info/METADATA +266 -0
- belgie_alchemy-0.1.0a4.dist-info/RECORD +28 -0
- belgie_alchemy-0.1.0a4.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""SQLAlchemy 2.0 building blocks for database models.
|
|
2
|
+
|
|
3
|
+
This module provides opinionated defaults and utilities for SQLAlchemy:
|
|
4
|
+
- Base: Declarative base with dataclass mapping and sensible defaults
|
|
5
|
+
- Mixins: PrimaryKeyMixin (UUID), TimestampMixin (created/updated/deleted)
|
|
6
|
+
- Types: DateTimeUTC (timezone-aware datetime storage)
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from belgie_alchemy import Base, PrimaryKeyMixin, TimestampMixin, DateTimeUTC
|
|
10
|
+
|
|
11
|
+
class MyModel(Base, PrimaryKeyMixin, TimestampMixin):
|
|
12
|
+
__tablename__ = "my_models"
|
|
13
|
+
|
|
14
|
+
name: Mapped[str]
|
|
15
|
+
created_on: Mapped[datetime] = mapped_column(DateTimeUTC)
|
|
16
|
+
|
|
17
|
+
For complete auth model examples, see examples/alchemy/auth_models.py
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from belgie_alchemy.adapter import AlchemyAdapter
|
|
21
|
+
from belgie_alchemy.base import Base
|
|
22
|
+
from belgie_alchemy.mixins import PrimaryKeyMixin, TimestampMixin
|
|
23
|
+
from belgie_alchemy.settings import DatabaseSettings
|
|
24
|
+
from belgie_alchemy.types import DateTimeUTC
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"AlchemyAdapter",
|
|
28
|
+
"Base",
|
|
29
|
+
"DatabaseSettings",
|
|
30
|
+
"DateTimeUTC",
|
|
31
|
+
"PrimaryKeyMixin",
|
|
32
|
+
"TimestampMixin",
|
|
33
|
+
]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
from datetime import UTC, datetime, timedelta
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
+
|
|
7
|
+
from belgie_alchemy import AlchemyAdapter
|
|
8
|
+
from belgie_alchemy.__tests__.fixtures.models import Account, OAuthState, Session, User
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def adapter(alchemy_session: AsyncSession) -> AlchemyAdapter: # noqa: ARG001
|
|
13
|
+
return AlchemyAdapter(
|
|
14
|
+
user=User,
|
|
15
|
+
account=Account,
|
|
16
|
+
session=Session,
|
|
17
|
+
oauth_state=OAuthState,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.mark.asyncio
|
|
22
|
+
async def test_create_user(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
23
|
+
user = await adapter.create_user(
|
|
24
|
+
alchemy_session,
|
|
25
|
+
email="test@example.com",
|
|
26
|
+
name="Test User",
|
|
27
|
+
email_verified=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
assert user.email == "test@example.com"
|
|
31
|
+
assert user.name == "Test User"
|
|
32
|
+
assert user.email_verified is True
|
|
33
|
+
assert user.id is not None
|
|
34
|
+
assert user.created_at is not None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
async def test_get_user_by_id(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
39
|
+
created_user = await adapter.create_user(
|
|
40
|
+
alchemy_session,
|
|
41
|
+
email="test@example.com",
|
|
42
|
+
name="Test User",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
found_user = await adapter.get_user_by_id(alchemy_session, created_user.id)
|
|
46
|
+
|
|
47
|
+
assert found_user is not None
|
|
48
|
+
assert found_user.id == created_user.id
|
|
49
|
+
assert found_user.email == "test@example.com"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.asyncio
|
|
53
|
+
async def test_get_user_by_id_not_found(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
54
|
+
user = await adapter.get_user_by_id(alchemy_session, uuid4())
|
|
55
|
+
assert user is None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_get_user_by_email(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
60
|
+
await adapter.create_user(
|
|
61
|
+
alchemy_session,
|
|
62
|
+
email="test@example.com",
|
|
63
|
+
name="Test User",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
user = await adapter.get_user_by_email(alchemy_session, "test@example.com")
|
|
67
|
+
|
|
68
|
+
assert user is not None
|
|
69
|
+
assert user.email == "test@example.com"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.mark.asyncio
|
|
73
|
+
async def test_get_user_by_email_not_found(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
74
|
+
user = await adapter.get_user_by_email(alchemy_session, "nonexistent@example.com")
|
|
75
|
+
assert user is None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
async def test_update_user(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
80
|
+
user = await adapter.create_user(
|
|
81
|
+
alchemy_session,
|
|
82
|
+
email="test@example.com",
|
|
83
|
+
name="Test User",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
updated_user = await adapter.update_user(
|
|
87
|
+
alchemy_session,
|
|
88
|
+
user.id,
|
|
89
|
+
name="Updated Name",
|
|
90
|
+
email_verified=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
assert updated_user is not None
|
|
94
|
+
assert updated_user.name == "Updated Name"
|
|
95
|
+
assert updated_user.email_verified is True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_update_user_not_found(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
100
|
+
updated_user = await adapter.update_user(
|
|
101
|
+
alchemy_session,
|
|
102
|
+
uuid4(),
|
|
103
|
+
name="Updated Name",
|
|
104
|
+
)
|
|
105
|
+
assert updated_user is None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_create_account(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
110
|
+
user = await adapter.create_user(
|
|
111
|
+
alchemy_session,
|
|
112
|
+
email="test@example.com",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
account = await adapter.create_account(
|
|
116
|
+
alchemy_session,
|
|
117
|
+
user_id=user.id,
|
|
118
|
+
provider="google",
|
|
119
|
+
provider_account_id="12345",
|
|
120
|
+
access_token="token",
|
|
121
|
+
refresh_token="refresh",
|
|
122
|
+
expires_at=datetime.now(UTC) + timedelta(hours=1),
|
|
123
|
+
token_type="Bearer",
|
|
124
|
+
scope="openid email",
|
|
125
|
+
id_token="id_token",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
assert account.user_id == user.id
|
|
129
|
+
assert account.provider == "google"
|
|
130
|
+
assert account.provider_account_id == "12345"
|
|
131
|
+
assert account.access_token == "token" # noqa: S105
|
|
132
|
+
assert account.refresh_token == "refresh" # noqa: S105
|
|
133
|
+
assert account.token_type == "Bearer" # noqa: S105
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.asyncio
|
|
137
|
+
async def test_get_account(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
138
|
+
user = await adapter.create_user(
|
|
139
|
+
alchemy_session,
|
|
140
|
+
email="test@example.com",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
await adapter.create_account(
|
|
144
|
+
alchemy_session,
|
|
145
|
+
user_id=user.id,
|
|
146
|
+
provider="google",
|
|
147
|
+
provider_account_id="12345",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
account = await adapter.get_account(alchemy_session, "google", "12345")
|
|
151
|
+
|
|
152
|
+
assert account is not None
|
|
153
|
+
assert account.provider == "google"
|
|
154
|
+
assert account.provider_account_id == "12345"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.asyncio
|
|
158
|
+
async def test_get_account_not_found(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
159
|
+
account = await adapter.get_account(alchemy_session, "google", "nonexistent")
|
|
160
|
+
assert account is None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.asyncio
|
|
164
|
+
async def test_create_session(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
165
|
+
user = await adapter.create_user(
|
|
166
|
+
alchemy_session,
|
|
167
|
+
email="test@example.com",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
expires_at = datetime.now(UTC) + timedelta(days=7)
|
|
171
|
+
session = await adapter.create_session(
|
|
172
|
+
alchemy_session,
|
|
173
|
+
user_id=user.id,
|
|
174
|
+
expires_at=expires_at,
|
|
175
|
+
ip_address="127.0.0.1",
|
|
176
|
+
user_agent="Test Agent",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
assert session.user_id == user.id
|
|
180
|
+
assert session.expires_at.replace(tzinfo=UTC) == expires_at
|
|
181
|
+
assert session.ip_address == "127.0.0.1"
|
|
182
|
+
assert session.user_agent == "Test Agent"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@pytest.mark.asyncio
|
|
186
|
+
async def test_get_session(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
187
|
+
user = await adapter.create_user(
|
|
188
|
+
alchemy_session,
|
|
189
|
+
email="test@example.com",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
created_session = await adapter.create_session(
|
|
193
|
+
alchemy_session,
|
|
194
|
+
user_id=user.id,
|
|
195
|
+
expires_at=datetime.now(UTC) + timedelta(days=7),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
found_session = await adapter.get_session(alchemy_session, created_session.id)
|
|
199
|
+
|
|
200
|
+
assert found_session is not None
|
|
201
|
+
assert found_session.id == created_session.id
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@pytest.mark.asyncio
|
|
205
|
+
async def test_get_session_not_found(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
206
|
+
session = await adapter.get_session(alchemy_session, uuid4())
|
|
207
|
+
assert session is None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@pytest.mark.asyncio
|
|
211
|
+
async def test_update_session(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
212
|
+
user = await adapter.create_user(
|
|
213
|
+
alchemy_session,
|
|
214
|
+
email="test@example.com",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
session = await adapter.create_session(
|
|
218
|
+
alchemy_session,
|
|
219
|
+
user_id=user.id,
|
|
220
|
+
expires_at=datetime.now(UTC) + timedelta(days=7),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
new_expires = datetime.now(UTC) + timedelta(days=14)
|
|
224
|
+
updated_session = await adapter.update_session(
|
|
225
|
+
alchemy_session,
|
|
226
|
+
session.id,
|
|
227
|
+
expires_at=new_expires,
|
|
228
|
+
ip_address="192.168.1.1",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
assert updated_session is not None
|
|
232
|
+
assert updated_session.expires_at.replace(tzinfo=UTC) == new_expires
|
|
233
|
+
assert updated_session.ip_address == "192.168.1.1"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@pytest.mark.asyncio
|
|
237
|
+
async def test_update_session_not_found(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
238
|
+
updated_session = await adapter.update_session(
|
|
239
|
+
alchemy_session,
|
|
240
|
+
uuid4(),
|
|
241
|
+
expires_at=datetime.now(UTC) + timedelta(days=7),
|
|
242
|
+
)
|
|
243
|
+
assert updated_session is None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.mark.asyncio
|
|
247
|
+
async def test_delete_session(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
248
|
+
user = await adapter.create_user(
|
|
249
|
+
alchemy_session,
|
|
250
|
+
email="test@example.com",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
session = await adapter.create_session(
|
|
254
|
+
alchemy_session,
|
|
255
|
+
user_id=user.id,
|
|
256
|
+
expires_at=datetime.now(UTC) + timedelta(days=7),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
deleted = await adapter.delete_session(alchemy_session, session.id)
|
|
260
|
+
assert deleted is True
|
|
261
|
+
|
|
262
|
+
found = await adapter.get_session(alchemy_session, session.id)
|
|
263
|
+
assert found is None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@pytest.mark.asyncio
|
|
267
|
+
async def test_delete_session_not_found(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
268
|
+
deleted = await adapter.delete_session(alchemy_session, uuid4())
|
|
269
|
+
assert deleted is False
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@pytest.mark.asyncio
|
|
273
|
+
async def test_delete_expired_sessions(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
274
|
+
user = await adapter.create_user(
|
|
275
|
+
alchemy_session,
|
|
276
|
+
email="test@example.com",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
await adapter.create_session(
|
|
280
|
+
alchemy_session,
|
|
281
|
+
user_id=user.id,
|
|
282
|
+
expires_at=datetime.now(UTC) - timedelta(days=1),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
await adapter.create_session(
|
|
286
|
+
alchemy_session,
|
|
287
|
+
user_id=user.id,
|
|
288
|
+
expires_at=datetime.now(UTC) + timedelta(days=7),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
count = await adapter.delete_expired_sessions(alchemy_session)
|
|
292
|
+
assert count == 1
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@pytest.mark.asyncio
|
|
296
|
+
async def test_create_oauth_state(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
297
|
+
expires_at = datetime.now(UTC) + timedelta(minutes=10)
|
|
298
|
+
oauth_state = await adapter.create_oauth_state(
|
|
299
|
+
alchemy_session,
|
|
300
|
+
state="random_state_123",
|
|
301
|
+
expires_at=expires_at,
|
|
302
|
+
code_verifier="verifier_abc",
|
|
303
|
+
redirect_url="/dashboard",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
assert oauth_state.state == "random_state_123"
|
|
307
|
+
assert oauth_state.code_verifier == "verifier_abc"
|
|
308
|
+
assert oauth_state.redirect_url == "/dashboard"
|
|
309
|
+
assert oauth_state.expires_at.replace(tzinfo=UTC) == expires_at
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@pytest.mark.asyncio
|
|
313
|
+
async def test_get_oauth_state(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
314
|
+
await adapter.create_oauth_state(
|
|
315
|
+
alchemy_session,
|
|
316
|
+
state="random_state_123",
|
|
317
|
+
expires_at=datetime.now(UTC) + timedelta(minutes=10),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
oauth_state = await adapter.get_oauth_state(alchemy_session, "random_state_123")
|
|
321
|
+
|
|
322
|
+
assert oauth_state is not None
|
|
323
|
+
assert oauth_state.state == "random_state_123"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@pytest.mark.asyncio
|
|
327
|
+
async def test_get_oauth_state_not_found(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
328
|
+
oauth_state = await adapter.get_oauth_state(alchemy_session, "nonexistent")
|
|
329
|
+
assert oauth_state is None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@pytest.mark.asyncio
|
|
333
|
+
async def test_delete_oauth_state(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
334
|
+
await adapter.create_oauth_state(
|
|
335
|
+
alchemy_session,
|
|
336
|
+
state="random_state_123",
|
|
337
|
+
expires_at=datetime.now(UTC) + timedelta(minutes=10),
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
deleted = await adapter.delete_oauth_state(alchemy_session, "random_state_123")
|
|
341
|
+
assert deleted is True
|
|
342
|
+
|
|
343
|
+
found = await adapter.get_oauth_state(alchemy_session, "random_state_123")
|
|
344
|
+
assert found is None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@pytest.mark.asyncio
|
|
348
|
+
async def test_delete_oauth_state_not_found(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
349
|
+
deleted = await adapter.delete_oauth_state(alchemy_session, "nonexistent")
|
|
350
|
+
assert deleted is False
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@pytest.mark.asyncio
|
|
354
|
+
async def test_user_with_custom_fields(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
355
|
+
user_data = User(
|
|
356
|
+
email="custom@example.com",
|
|
357
|
+
email_verified=True,
|
|
358
|
+
name="Custom User",
|
|
359
|
+
image=None,
|
|
360
|
+
custom_field="custom value",
|
|
361
|
+
)
|
|
362
|
+
alchemy_session.add(user_data)
|
|
363
|
+
await alchemy_session.commit()
|
|
364
|
+
|
|
365
|
+
found = await adapter.get_user_by_email(alchemy_session, "custom@example.com")
|
|
366
|
+
assert found is not None
|
|
367
|
+
assert found.custom_field == "custom value"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@pytest.mark.asyncio
|
|
371
|
+
async def test_delete_user_deletes_user(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
372
|
+
user = await adapter.create_user(alchemy_session, email="delete@example.com", name="Delete User")
|
|
373
|
+
|
|
374
|
+
deleted = await adapter.delete_user(alchemy_session, user.id)
|
|
375
|
+
|
|
376
|
+
assert deleted is True
|
|
377
|
+
assert await adapter.get_user_by_id(alchemy_session, user.id) is None
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@pytest.mark.asyncio
|
|
381
|
+
async def test_delete_user_deletes_sessions(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
382
|
+
user = await adapter.create_user(alchemy_session, email="delete@example.com", name="Delete User")
|
|
383
|
+
|
|
384
|
+
expires_at = datetime.now(UTC) + timedelta(days=1)
|
|
385
|
+
session1 = await adapter.create_session(alchemy_session, user.id, expires_at.replace(tzinfo=None))
|
|
386
|
+
session2 = await adapter.create_session(alchemy_session, user.id, expires_at.replace(tzinfo=None))
|
|
387
|
+
|
|
388
|
+
await adapter.delete_user(alchemy_session, user.id)
|
|
389
|
+
|
|
390
|
+
assert await adapter.get_session(alchemy_session, session1.id) is None
|
|
391
|
+
assert await adapter.get_session(alchemy_session, session2.id) is None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@pytest.mark.asyncio
|
|
395
|
+
async def test_delete_user_deletes_accounts(adapter: AlchemyAdapter, alchemy_session: AsyncSession) -> None:
|
|
396
|
+
user = await adapter.create_user(alchemy_session, email="delete@example.com", name="Delete User")
|
|
397
|
+
|
|
398
|
+
await adapter.create_account(
|
|
399
|
+
alchemy_session,
|
|
400
|
+
user.id,
|
|
401
|
+
"google",
|
|
402
|
+
"google-123",
|
|
403
|
+
access_token="token123",
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
await adapter.delete_user(alchemy_session, user.id)
|
|
407
|
+
|
|
408
|
+
assert await adapter.get_account(alchemy_session, "google", "google-123") is None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@pytest.mark.asyncio
|
|
412
|
+
async def test_delete_user_deletes_all_related_data(
|
|
413
|
+
adapter: AlchemyAdapter,
|
|
414
|
+
alchemy_session: AsyncSession,
|
|
415
|
+
) -> None:
|
|
416
|
+
user = await adapter.create_user(alchemy_session, email="delete@example.com", name="Delete User")
|
|
417
|
+
|
|
418
|
+
expires_at = datetime.now(UTC) + timedelta(days=1)
|
|
419
|
+
session1 = await adapter.create_session(alchemy_session, user.id, expires_at.replace(tzinfo=None))
|
|
420
|
+
session2 = await adapter.create_session(alchemy_session, user.id, expires_at.replace(tzinfo=None))
|
|
421
|
+
|
|
422
|
+
await adapter.create_account(
|
|
423
|
+
alchemy_session,
|
|
424
|
+
user.id,
|
|
425
|
+
"google",
|
|
426
|
+
"google-123",
|
|
427
|
+
access_token="token123",
|
|
428
|
+
)
|
|
429
|
+
await adapter.create_account(
|
|
430
|
+
alchemy_session,
|
|
431
|
+
user.id,
|
|
432
|
+
"github",
|
|
433
|
+
"github-456",
|
|
434
|
+
access_token="token456",
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
deleted = await adapter.delete_user(alchemy_session, user.id)
|
|
438
|
+
|
|
439
|
+
assert deleted is True
|
|
440
|
+
assert await adapter.get_user_by_id(alchemy_session, user.id) is None
|
|
441
|
+
assert await adapter.get_session(alchemy_session, session1.id) is None
|
|
442
|
+
assert await adapter.get_session(alchemy_session, session2.id) is None
|
|
443
|
+
assert await adapter.get_account(alchemy_session, "google", "google-123") is None
|
|
444
|
+
assert await adapter.get_account(alchemy_session, "github", "github-456") is None
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@pytest.mark.asyncio
|
|
448
|
+
async def test_delete_user_returns_false_if_user_not_found(
|
|
449
|
+
adapter: AlchemyAdapter,
|
|
450
|
+
alchemy_session: AsyncSession,
|
|
451
|
+
) -> None:
|
|
452
|
+
fake_user_id = uuid4()
|
|
453
|
+
deleted = await adapter.delete_user(alchemy_session, fake_user_id)
|
|
454
|
+
|
|
455
|
+
assert deleted is False
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@pytest.mark.asyncio
|
|
459
|
+
async def test_delete_user_only_deletes_target_users_data(
|
|
460
|
+
adapter: AlchemyAdapter,
|
|
461
|
+
alchemy_session: AsyncSession,
|
|
462
|
+
) -> None:
|
|
463
|
+
user1 = await adapter.create_user(alchemy_session, email="user1@example.com", name="User 1")
|
|
464
|
+
user2 = await adapter.create_user(alchemy_session, email="user2@example.com", name="User 2")
|
|
465
|
+
|
|
466
|
+
expires_at = datetime.now(UTC) + timedelta(days=1)
|
|
467
|
+
session1 = await adapter.create_session(alchemy_session, user1.id, expires_at.replace(tzinfo=None))
|
|
468
|
+
session2 = await adapter.create_session(alchemy_session, user2.id, expires_at.replace(tzinfo=None))
|
|
469
|
+
|
|
470
|
+
await adapter.create_account(
|
|
471
|
+
alchemy_session,
|
|
472
|
+
user1.id,
|
|
473
|
+
"google",
|
|
474
|
+
"google-user1",
|
|
475
|
+
access_token="token1",
|
|
476
|
+
)
|
|
477
|
+
await adapter.create_account(
|
|
478
|
+
alchemy_session,
|
|
479
|
+
user2.id,
|
|
480
|
+
"google",
|
|
481
|
+
"google-user2",
|
|
482
|
+
access_token="token2",
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
await adapter.delete_user(alchemy_session, user1.id)
|
|
486
|
+
|
|
487
|
+
assert await adapter.get_user_by_id(alchemy_session, user1.id) is None
|
|
488
|
+
assert await adapter.get_session(alchemy_session, session1.id) is None
|
|
489
|
+
assert await adapter.get_account(alchemy_session, "google", "google-user1") is None
|
|
490
|
+
|
|
491
|
+
assert await adapter.get_user_by_id(alchemy_session, user2.id) is not None
|
|
492
|
+
assert await adapter.get_session(alchemy_session, session2.id) is not None
|
|
493
|
+
assert await adapter.get_account(alchemy_session, "google", "google-user2") is not None
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from datetime import UTC, datetime, timedelta
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from sqlalchemy import select
|
|
5
|
+
from sqlalchemy.exc import IntegrityError
|
|
6
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
7
|
+
|
|
8
|
+
from belgie_alchemy.__tests__.fixtures.models import Account, OAuthState, Session, User
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_user_model_structure() -> None:
|
|
12
|
+
"""Verify User model demonstrates proper structure."""
|
|
13
|
+
assert User.__tablename__ == "users"
|
|
14
|
+
assert not getattr(User, "__abstract__", False)
|
|
15
|
+
assert hasattr(User, "email")
|
|
16
|
+
assert hasattr(User, "scopes")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_user_has_scopes_field() -> None:
|
|
20
|
+
"""Verify User has scopes field that accepts list of strings."""
|
|
21
|
+
user = User(email="test@example.com")
|
|
22
|
+
user.scopes = ["read", "write"]
|
|
23
|
+
assert user.scopes == ["read", "write"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_user_relationships_defined() -> None:
|
|
27
|
+
"""Verify User has bidirectional relationships defined."""
|
|
28
|
+
assert hasattr(User, "accounts")
|
|
29
|
+
assert hasattr(User, "sessions")
|
|
30
|
+
assert hasattr(User, "oauth_states")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.asyncio
|
|
34
|
+
async def test_account_unique_constraint(alchemy_session: AsyncSession) -> None:
|
|
35
|
+
user = User(email="auth@example.com")
|
|
36
|
+
alchemy_session.add(user)
|
|
37
|
+
await alchemy_session.commit()
|
|
38
|
+
|
|
39
|
+
account = Account(
|
|
40
|
+
user_id=user.id,
|
|
41
|
+
provider="google",
|
|
42
|
+
provider_account_id="abc",
|
|
43
|
+
)
|
|
44
|
+
alchemy_session.add(account)
|
|
45
|
+
await alchemy_session.commit()
|
|
46
|
+
|
|
47
|
+
duplicate = Account(
|
|
48
|
+
user_id=user.id,
|
|
49
|
+
provider="google",
|
|
50
|
+
provider_account_id="abc",
|
|
51
|
+
)
|
|
52
|
+
alchemy_session.add(duplicate)
|
|
53
|
+
with pytest.raises(IntegrityError):
|
|
54
|
+
await alchemy_session.commit()
|
|
55
|
+
await alchemy_session.rollback()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_session_relationship(alchemy_session: AsyncSession) -> None:
|
|
60
|
+
user = User(email="session@example.com")
|
|
61
|
+
alchemy_session.add(user)
|
|
62
|
+
await alchemy_session.commit()
|
|
63
|
+
|
|
64
|
+
session = Session(
|
|
65
|
+
user_id=user.id,
|
|
66
|
+
expires_at=datetime.now(UTC) + timedelta(days=1),
|
|
67
|
+
)
|
|
68
|
+
alchemy_session.add(session)
|
|
69
|
+
await alchemy_session.commit()
|
|
70
|
+
|
|
71
|
+
refreshed = await alchemy_session.get(User, user.id)
|
|
72
|
+
assert refreshed is not None
|
|
73
|
+
await alchemy_session.refresh(refreshed, attribute_names=["sessions"])
|
|
74
|
+
assert len(refreshed.sessions) == 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.asyncio
|
|
78
|
+
async def test_oauth_state_optional_user(alchemy_session: AsyncSession) -> None:
|
|
79
|
+
state = OAuthState(
|
|
80
|
+
state="abc",
|
|
81
|
+
code_verifier=None,
|
|
82
|
+
redirect_url=None,
|
|
83
|
+
expires_at=datetime.now(UTC) + timedelta(minutes=5),
|
|
84
|
+
user_id=None,
|
|
85
|
+
)
|
|
86
|
+
alchemy_session.add(state)
|
|
87
|
+
await alchemy_session.commit()
|
|
88
|
+
|
|
89
|
+
rows = await alchemy_session.execute(select(OAuthState))
|
|
90
|
+
stored = rows.scalar_one()
|
|
91
|
+
assert stored.user is None
|
|
File without changes
|