|
2 | 2 | # pylint: disable=unused-argument |
3 | 3 | # pylint: disable=unused-variable |
4 | 4 |
|
| 5 | +import asyncio |
5 | 6 | import json |
6 | 7 | import time |
| 8 | +from collections.abc import AsyncIterator, Callable |
| 9 | +from contextlib import AsyncExitStack |
7 | 10 | from http import HTTPStatus |
8 | 11 |
|
9 | 12 | import pytest |
10 | | -from aiohttp.test_utils import TestClient |
| 13 | +from aiohttp.test_utils import TestClient, TestServer |
11 | 14 | from cryptography import fernet |
12 | 15 | from faker import Faker |
13 | 16 | from pytest_simcore.helpers.assert_checks import assert_status |
@@ -198,3 +201,73 @@ def _build_proxy_session_cookie(identity: str): |
198 | 201 |
|
199 | 202 | if not error: |
200 | 203 | assert data["login"] == user["email"] |
| 204 | + |
| 205 | + |
| 206 | +@pytest.fixture |
| 207 | +async def multiple_users( |
| 208 | + client: TestClient, num_users: int = 5 |
| 209 | +) -> AsyncIterator[list[dict[str, str]]]: |
| 210 | + """Fixture that creates multiple test users with an AsyncExitStack for cleanup.""" |
| 211 | + async with AsyncExitStack() as exit_stack: |
| 212 | + users = [] |
| 213 | + for _ in range(num_users): |
| 214 | + # Use enter_async_context to properly register each NewUser context manager |
| 215 | + user_ctx = await exit_stack.enter_async_context(NewUser(app=client.app)) |
| 216 | + users.append( |
| 217 | + { |
| 218 | + "email": user_ctx["email"], |
| 219 | + "password": user_ctx["raw_password"], |
| 220 | + } |
| 221 | + ) |
| 222 | + |
| 223 | + yield users |
| 224 | + # AsyncExitStack will automatically clean up all users when exiting |
| 225 | + |
| 226 | + |
| 227 | +async def test_multiple_users_login_logout_concurrently( |
| 228 | + web_server: TestServer, |
| 229 | + client: TestClient, |
| 230 | + multiple_users: list[dict[str, str]], |
| 231 | + aiohttp_client: Callable, |
| 232 | +): |
| 233 | + """Test multiple users can login concurrently and properly get logged out.""" |
| 234 | + assert client.app |
| 235 | + |
| 236 | + # URLs |
| 237 | + login_url = client.app.router["auth_login"].url_for().path |
| 238 | + profile_url = client.app.router["get_my_profile"].url_for().path |
| 239 | + logout_url = client.app.router["auth_logout"].url_for().path |
| 240 | + |
| 241 | + async def user_session_flow(user_creds): |
| 242 | + # Create a new client for each user to ensure isolated sessions |
| 243 | + user_client = await aiohttp_client(web_server) |
| 244 | + |
| 245 | + # Login |
| 246 | + login_resp = await user_client.post( |
| 247 | + login_url, |
| 248 | + json={"email": user_creds["email"], "password": user_creds["password"]}, |
| 249 | + ) |
| 250 | + login_data, _ = await assert_status(login_resp, status.HTTP_200_OK) |
| 251 | + assert MSG_LOGGED_IN in login_data["message"] |
| 252 | + |
| 253 | + # Access profile (cookies are automatically sent by the client) |
| 254 | + profile_resp = await user_client.get(profile_url) |
| 255 | + profile_data, _ = await assert_status(profile_resp, status.HTTP_200_OK) |
| 256 | + assert profile_data["login"] == user_creds["email"] |
| 257 | + |
| 258 | + # Logout |
| 259 | + logout_resp = await user_client.post(logout_url) |
| 260 | + await assert_status(logout_resp, status.HTTP_200_OK) |
| 261 | + |
| 262 | + # Try to access profile after logout |
| 263 | + profile_after_logout_resp = await user_client.get(profile_url) |
| 264 | + _, error = await assert_status( |
| 265 | + profile_after_logout_resp, status.HTTP_401_UNAUTHORIZED |
| 266 | + ) |
| 267 | + |
| 268 | + # No need to manually close the client as aiohttp_client fixture handles cleanup |
| 269 | + |
| 270 | + await user_session_flow(multiple_users[0]) |
| 271 | + |
| 272 | + # Run all user flows concurrently |
| 273 | + await asyncio.gather(*(user_session_flow(user) for user in multiple_users)) |
0 commit comments