|
3 | 3 | # pylint: disable=unused-variable |
4 | 4 | # pylint: disable=too-many-arguments |
5 | 5 |
|
| 6 | +import time |
6 | 7 | from collections.abc import AsyncGenerator, AsyncIterator, Callable |
7 | 8 |
|
8 | 9 | import pytest |
| 10 | +from aiocache import Cache |
9 | 11 | from asgi_lifespan import LifespanManager |
10 | 12 | from fastapi import FastAPI |
11 | 13 | from fastapi.security import HTTPBasicCredentials |
12 | 14 | from models_library.api_schemas_api_server.api_keys import ApiKeyInDB |
13 | 15 | from pydantic import PositiveInt |
| 16 | +from pytest_mock import MockerFixture |
14 | 17 | from simcore_service_api_server.api.dependencies.authentication import ( |
15 | 18 | get_current_identity, |
16 | 19 | ) |
@@ -54,61 +57,156 @@ def users_repo( |
54 | 57 | return UsersRepository(db_engine=async_engine) |
55 | 58 |
|
56 | 59 |
|
57 | | -async def test_get_user_with_valid_credentials( |
| 60 | +@pytest.fixture |
| 61 | +async def api_key_in_db( |
58 | 62 | create_fake_api_keys: Callable[[PositiveInt], AsyncGenerator[ApiKeyInDB, None]], |
| 63 | +) -> ApiKeyInDB: |
| 64 | + """Creates a single API key in DB for testing purposes""" |
| 65 | + return await anext(create_fake_api_keys(1)) |
| 66 | + |
| 67 | + |
| 68 | +async def test_get_user_with_valid_credentials( |
| 69 | + api_key_in_db: ApiKeyInDB, |
59 | 70 | api_key_repo: ApiKeysRepository, |
60 | 71 | ): |
| 72 | + # Act |
| 73 | + result = await api_key_repo.get_user( |
| 74 | + api_key=api_key_in_db.api_key, api_secret=api_key_in_db.api_secret |
| 75 | + ) |
61 | 76 |
|
62 | | - # Generate a fake API key |
63 | | - async for api_key_in_db in create_fake_api_keys(1): |
64 | | - # Act |
65 | | - result = await api_key_repo.get_user( |
66 | | - api_key=api_key_in_db.api_key, api_secret=api_key_in_db.api_secret |
67 | | - ) |
68 | | - |
69 | | - # Assert |
70 | | - assert result is not None |
71 | | - assert result.user_id == api_key_in_db.user_id |
72 | | - assert result.product_name == api_key_in_db.product_name |
73 | | - break |
| 77 | + # Assert |
| 78 | + assert result is not None |
| 79 | + assert result.user_id == api_key_in_db.user_id |
| 80 | + assert result.product_name == api_key_in_db.product_name |
74 | 81 |
|
75 | 82 |
|
76 | 83 | async def test_get_user_with_invalid_credentials( |
| 84 | + api_key_in_db: ApiKeyInDB, |
77 | 85 | api_key_repo: ApiKeysRepository, |
78 | | - create_fake_api_keys: Callable[[PositiveInt], AsyncGenerator[ApiKeyInDB, None]], |
79 | 86 | ): |
80 | 87 |
|
81 | 88 | # Generate a fake API key |
82 | | - async for api_key_in_db in create_fake_api_keys(1): |
83 | | - # Act - use wrong secret |
84 | | - result = await api_key_repo.get_user( |
85 | | - api_key=api_key_in_db.api_key, api_secret="wrong_secret" |
86 | | - ) |
87 | 89 |
|
88 | | - # Assert |
89 | | - assert result is None |
90 | | - break |
| 90 | + # Act - use wrong secret |
| 91 | + result = await api_key_repo.get_user( |
| 92 | + api_key=api_key_in_db.api_key, api_secret="wrong_secret" |
| 93 | + ) |
| 94 | + |
| 95 | + # Assert |
| 96 | + assert result is None |
91 | 97 |
|
92 | 98 |
|
93 | 99 | async def test_rest_dependency_authentication( |
| 100 | + api_key_in_db: ApiKeyInDB, |
94 | 101 | api_key_repo: ApiKeysRepository, |
95 | 102 | users_repo: UsersRepository, |
96 | | - create_fake_api_keys: Callable[[PositiveInt], AsyncGenerator[ApiKeyInDB, None]], |
97 | 103 | ): |
98 | 104 |
|
99 | 105 | # Generate a fake API key |
100 | | - async for api_key_in_db in create_fake_api_keys(1): |
101 | | - # Act |
102 | | - result = await get_current_identity( |
103 | | - apikeys_repo=api_key_repo, |
104 | | - users_repo=users_repo, |
105 | | - credentials=HTTPBasicCredentials( |
106 | | - username=api_key_in_db.api_key, password=api_key_in_db.api_secret |
107 | | - ), |
108 | | - ) |
109 | | - |
110 | | - # Assert |
111 | | - assert result is not None |
112 | | - assert result.user_id == api_key_in_db.user_id |
113 | | - assert result.product_name == api_key_in_db.product_name |
114 | | - break |
| 106 | + # Act |
| 107 | + result = await get_current_identity( |
| 108 | + apikeys_repo=api_key_repo, |
| 109 | + users_repo=users_repo, |
| 110 | + credentials=HTTPBasicCredentials( |
| 111 | + username=api_key_in_db.api_key, password=api_key_in_db.api_secret |
| 112 | + ), |
| 113 | + ) |
| 114 | + |
| 115 | + # Assert |
| 116 | + assert result is not None |
| 117 | + assert result.user_id == api_key_in_db.user_id |
| 118 | + assert result.product_name == api_key_in_db.product_name |
| 119 | + |
| 120 | + |
| 121 | +async def test_cache_effectiveness_in_rest_authentication_dependencies( |
| 122 | + api_key_in_db: ApiKeyInDB, |
| 123 | + api_key_repo: ApiKeysRepository, |
| 124 | + users_repo: UsersRepository, |
| 125 | + monkeypatch: pytest.MonkeyPatch, |
| 126 | + mocker: MockerFixture, |
| 127 | +): |
| 128 | + """Test that caching reduces database calls and improves performance.""" |
| 129 | + |
| 130 | + # Generate a fake API key |
| 131 | + credentials = HTTPBasicCredentials( |
| 132 | + username=api_key_in_db.api_key, password=api_key_in_db.api_secret |
| 133 | + ) |
| 134 | + |
| 135 | + # Test with cache enabled (default) |
| 136 | + monkeypatch.delenv("AIOCACHE_DISABLE", raising=False) |
| 137 | + await Cache().clear() # Clear any existing cache |
| 138 | + |
| 139 | + # Spy on the connection's execute method by patching AsyncConnection.execute |
| 140 | + from sqlalchemy.ext.asyncio import AsyncConnection # noqa: PLC0415 |
| 141 | + |
| 142 | + execute_spy = mocker.spy(AsyncConnection, "execute") |
| 143 | + |
| 144 | + # First call - should hit database |
| 145 | + start_time = time.time() |
| 146 | + result1 = await get_current_identity( |
| 147 | + apikeys_repo=api_key_repo, |
| 148 | + users_repo=users_repo, |
| 149 | + credentials=credentials, |
| 150 | + ) |
| 151 | + first_call_time = time.time() - start_time |
| 152 | + |
| 153 | + # Second call - should use cache |
| 154 | + start_time = time.time() |
| 155 | + result2 = await get_current_identity( |
| 156 | + apikeys_repo=api_key_repo, |
| 157 | + users_repo=users_repo, |
| 158 | + credentials=credentials, |
| 159 | + ) |
| 160 | + second_call_time = time.time() - start_time |
| 161 | + |
| 162 | + cached_db_calls = execute_spy.call_count |
| 163 | + execute_spy.reset_mock() |
| 164 | + |
| 165 | + # Test with cache disabled |
| 166 | + monkeypatch.setenv("AIOCACHE_DISABLE", "1") |
| 167 | + |
| 168 | + # First call without cache |
| 169 | + start_time = time.time() |
| 170 | + result3 = await get_current_identity( |
| 171 | + apikeys_repo=api_key_repo, |
| 172 | + users_repo=users_repo, |
| 173 | + credentials=credentials, |
| 174 | + ) |
| 175 | + no_cache_first_time = time.time() - start_time |
| 176 | + |
| 177 | + # Second call without cache |
| 178 | + start_time = time.time() |
| 179 | + result4 = await get_current_identity( |
| 180 | + apikeys_repo=api_key_repo, |
| 181 | + users_repo=users_repo, |
| 182 | + credentials=credentials, |
| 183 | + ) |
| 184 | + no_cache_second_time = time.time() - start_time |
| 185 | + |
| 186 | + no_cache_db_calls = execute_spy.call_count |
| 187 | + |
| 188 | + # ASSERTIONS |
| 189 | + # All results should be identical |
| 190 | + assert result1.user_id == result2.user_id == result3.user_id == result4.user_id |
| 191 | + assert ( |
| 192 | + result1.product_name |
| 193 | + == result2.product_name |
| 194 | + == result3.product_name |
| 195 | + == result4.product_name |
| 196 | + ) |
| 197 | + assert result1.email == result2.email == result3.email == result4.email |
| 198 | + |
| 199 | + # With cache: second call should be significantly faster |
| 200 | + assert ( |
| 201 | + second_call_time < first_call_time * 0.5 |
| 202 | + ), "Cache should make subsequent calls faster" |
| 203 | + |
| 204 | + # Without cache: both calls should take similar time |
| 205 | + time_ratio = abs(no_cache_second_time - no_cache_first_time) / max( |
| 206 | + no_cache_first_time, no_cache_second_time |
| 207 | + ) |
| 208 | + assert time_ratio < 0.5, "Without cache, call times should be similar" |
| 209 | + |
| 210 | + # With cache: fewer total database calls (due to caching) |
| 211 | + # Without cache: more database calls (no caching) |
| 212 | + assert no_cache_db_calls > cached_db_calls, "Cache should reduce database calls" |
0 commit comments