Skip to content

Commit 512c702

Browse files
committed
refactor and split tests
1 parent 92e2ea2 commit 512c702

File tree

3 files changed

+163
-80
lines changed

3 files changed

+163
-80
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# pylint: disable=redefined-outer-name
2+
# pylint: disable=unused-argument
3+
# pylint: disable=unused-variable
4+
# pylint: disable=too-many-arguments
5+
6+
from collections.abc import AsyncGenerator, AsyncIterator, Callable
7+
8+
import pytest
9+
from asgi_lifespan import LifespanManager
10+
from fastapi import FastAPI
11+
from models_library.api_schemas_api_server.api_keys import ApiKeyInDB
12+
from pydantic import PositiveInt
13+
from simcore_service_api_server.clients.postgres import get_engine
14+
from simcore_service_api_server.repository.api_keys import ApiKeysRepository
15+
from simcore_service_api_server.repository.users import UsersRepository
16+
from sqlalchemy.ext.asyncio import AsyncEngine
17+
18+
MAX_TIME_FOR_APP_TO_STARTUP = 10
19+
MAX_TIME_FOR_APP_TO_SHUTDOWN = 10
20+
21+
22+
@pytest.fixture
23+
async def app_started(app: FastAPI, is_pdb_enabled: bool) -> AsyncIterator[FastAPI]:
24+
# LifespanManager will trigger app's startup&shutown event handlers
25+
async with LifespanManager(
26+
app,
27+
startup_timeout=None if is_pdb_enabled else MAX_TIME_FOR_APP_TO_STARTUP,
28+
shutdown_timeout=None if is_pdb_enabled else MAX_TIME_FOR_APP_TO_SHUTDOWN,
29+
):
30+
yield app
31+
32+
33+
@pytest.fixture
34+
async def async_engine(app_started: FastAPI) -> AsyncEngine:
35+
# Overrides
36+
return get_engine(app_started)
37+
38+
39+
@pytest.fixture
40+
def api_key_repo(
41+
async_engine: AsyncEngine,
42+
) -> ApiKeysRepository:
43+
return ApiKeysRepository(db_engine=async_engine)
44+
45+
46+
@pytest.fixture
47+
def users_repo(
48+
async_engine: AsyncEngine,
49+
) -> UsersRepository:
50+
return UsersRepository(db_engine=async_engine)
51+
52+
53+
@pytest.fixture
54+
async def api_key_in_db(
55+
create_fake_api_keys: Callable[[PositiveInt], AsyncGenerator[ApiKeyInDB, None]],
56+
) -> ApiKeyInDB:
57+
"""Creates a single API key in DB for testing purposes"""
58+
return await anext(create_fake_api_keys(1))

services/api-server/tests/unit/_with_db/test_repository_api_keys.py renamed to services/api-server/tests/unit/_with_db/authentication/test_api_dependency_authentication.py

Lines changed: 66 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,96 +4,17 @@
44
# pylint: disable=too-many-arguments
55

66
import time
7-
from collections.abc import AsyncGenerator, AsyncIterator, Callable
87

98
import pytest
109
from aiocache import Cache
11-
from asgi_lifespan import LifespanManager
12-
from fastapi import FastAPI
1310
from fastapi.security import HTTPBasicCredentials
1411
from models_library.api_schemas_api_server.api_keys import ApiKeyInDB
15-
from pydantic import PositiveInt
1612
from pytest_mock import MockerFixture
1713
from simcore_service_api_server.api.dependencies.authentication import (
1814
get_current_identity,
1915
)
20-
from simcore_service_api_server.clients.postgres import get_engine
2116
from simcore_service_api_server.repository.api_keys import ApiKeysRepository
2217
from simcore_service_api_server.repository.users import UsersRepository
23-
from sqlalchemy.ext.asyncio import AsyncEngine
24-
25-
MAX_TIME_FOR_APP_TO_STARTUP = 10
26-
MAX_TIME_FOR_APP_TO_SHUTDOWN = 10
27-
28-
29-
@pytest.fixture
30-
async def app_started(app: FastAPI, is_pdb_enabled: bool) -> AsyncIterator[FastAPI]:
31-
# LifespanManager will trigger app's startup&shutown event handlers
32-
async with LifespanManager(
33-
app,
34-
startup_timeout=None if is_pdb_enabled else MAX_TIME_FOR_APP_TO_STARTUP,
35-
shutdown_timeout=None if is_pdb_enabled else MAX_TIME_FOR_APP_TO_SHUTDOWN,
36-
):
37-
yield app
38-
39-
40-
@pytest.fixture
41-
async def async_engine(app_started: FastAPI) -> AsyncEngine:
42-
# Overrides
43-
return get_engine(app_started)
44-
45-
46-
@pytest.fixture
47-
def api_key_repo(
48-
async_engine: AsyncEngine,
49-
) -> ApiKeysRepository:
50-
return ApiKeysRepository(db_engine=async_engine)
51-
52-
53-
@pytest.fixture
54-
def users_repo(
55-
async_engine: AsyncEngine,
56-
) -> UsersRepository:
57-
return UsersRepository(db_engine=async_engine)
58-
59-
60-
@pytest.fixture
61-
async def api_key_in_db(
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,
70-
api_key_repo: ApiKeysRepository,
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-
)
76-
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
81-
82-
83-
async def test_get_user_with_invalid_credentials(
84-
api_key_in_db: ApiKeyInDB,
85-
api_key_repo: ApiKeysRepository,
86-
):
87-
88-
# Generate a fake API key
89-
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
9718

9819

9920
async def test_rest_dependency_authentication(
@@ -125,7 +46,72 @@ async def test_cache_effectiveness_in_rest_authentication_dependencies(
12546
monkeypatch: pytest.MonkeyPatch,
12647
mocker: MockerFixture,
12748
):
128-
"""Test that caching reduces database calls and improves performance."""
49+
"""Test that caching reduces database calls and improves performance.
50+
51+
## Memory Implications of aiocache
52+
53+
### Memory Usage Characteristics
54+
55+
**aiocache** uses in-memory storage by default, which means:
56+
57+
1. **Linear memory growth**: Each cached item consumes RAM proportional to the serialized size of the cached data
58+
2. **No automatic memory limits**: By default, there's no built-in maximum memory cap
59+
3. **TTL-based cleanup**: Items are only removed when they expire (TTL) or are explicitly deleted
60+
61+
### Memory Limits & Configuration
62+
63+
**Available configuration options:**
64+
65+
```python
66+
# Memory backend configuration
67+
cache = Cache(Cache.MEMORY, **{
68+
'serializer': {
69+
'class': 'aiocache.serializers.PickleSerializer'
70+
},
71+
# No built-in memory limit options for MEMORY backend
72+
})
73+
```
74+
75+
**Key limitations:**
76+
- **MEMORY backend**: No built-in memory limits or LRU eviction
77+
- **Maximum capacity**: Limited only by available system RAM
78+
- **Risk**: Memory leaks if TTL is too long or cache keys grow unbounded
79+
80+
### Recommendations for Your Use Case
81+
82+
**For authentication caching:**
83+
84+
1. **Low memory impact**: User authentication data is typically small (user_id, email, product_name)
85+
2. **Short TTL**: Your 120s TTL helps prevent unbounded growth
86+
3. **Bounded key space**: API keys are finite, not user-generated
87+
88+
**Memory estimation:**
89+
```
90+
Per cache entry ≈ 200-500 bytes (user data + overhead)
91+
1000 active users ≈ 500KB
92+
10000 active users ≈ 5MB
93+
```
94+
95+
### Alternative Approaches
96+
97+
**If memory becomes a concern:**
98+
99+
1. **Redis backend**:
100+
```python
101+
cache = Cache(Cache.REDIS, endpoint="redis://localhost", ...)
102+
```
103+
104+
2. **Custom eviction policy**: Implement LRU manually or use shorter TTL
105+
106+
3. **Monitoring**: Track cache size in production:
107+
```python
108+
# Check cache statistics
109+
cache_stats = await cache.get_stats()
110+
```
111+
112+
**Verdict**:
113+
For authentication use case with reasonable user counts (<10K active), memory impact should be minimal with your current TTL configuration.
114+
"""
129115

130116
# Generate a fake API key
131117
credentials = HTTPBasicCredentials(
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# pylint: disable=redefined-outer-name
2+
# pylint: disable=unused-argument
3+
# pylint: disable=unused-variable
4+
# pylint: disable=too-many-arguments
5+
6+
7+
from models_library.api_schemas_api_server.api_keys import ApiKeyInDB
8+
from simcore_service_api_server.repository.api_keys import ApiKeysRepository
9+
10+
11+
async def test_get_user_with_valid_credentials(
12+
api_key_in_db: ApiKeyInDB,
13+
api_key_repo: ApiKeysRepository,
14+
):
15+
# Act
16+
result = await api_key_repo.get_user(
17+
api_key=api_key_in_db.api_key, api_secret=api_key_in_db.api_secret
18+
)
19+
20+
# Assert
21+
assert result is not None
22+
assert result.user_id == api_key_in_db.user_id
23+
assert result.product_name == api_key_in_db.product_name
24+
25+
26+
async def test_get_user_with_invalid_credentials(
27+
api_key_in_db: ApiKeyInDB,
28+
api_key_repo: ApiKeysRepository,
29+
):
30+
31+
# Generate a fake API key
32+
33+
# Act - use wrong secret
34+
result = await api_key_repo.get_user(
35+
api_key=api_key_in_db.api_key, api_secret="wrong_secret"
36+
)
37+
38+
# Assert
39+
assert result is None

0 commit comments

Comments
 (0)