Skip to content

Commit 9d33e95

Browse files
committed
split login from users
1 parent 1fa6877 commit 9d33e95

File tree

11 files changed

+190
-160
lines changed

11 files changed

+190
-160
lines changed

packages/pytest-simcore/src/pytest_simcore/helpers/webserver_login.py

Lines changed: 8 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,21 @@
11
import contextlib
22
import re
33
from collections.abc import AsyncIterator
4-
from datetime import datetime
5-
from typing import Any, TypedDict
4+
from typing import Any
65

7-
from aiohttp import web
86
from aiohttp.test_utils import TestClient
9-
from models_library.users import UserID
107
from servicelib.aiohttp import status
11-
from simcore_postgres_database.models.users import users as users_table
12-
from simcore_service_webserver.db.models import UserRole, UserStatus
13-
from simcore_service_webserver.db.plugin import get_asyncpg_engine
14-
from simcore_service_webserver.groups import api as groups_service
158
from simcore_service_webserver.login._constants import MSG_LOGGED_IN
169
from simcore_service_webserver.login._invitations_service import create_invitation_token
17-
from simcore_service_webserver.products.products_service import list_products
10+
from simcore_service_webserver.login._login_repository_legacy import (
11+
get_plugin_storage,
12+
)
1813
from simcore_service_webserver.security import security_service
19-
from sqlalchemy.ext.asyncio import AsyncEngine
2014
from yarl import URL
2115

2216
from .assert_checks import assert_status
23-
from .faker_factories import DEFAULT_FAKER, DEFAULT_TEST_PASSWORD, random_user
24-
from .postgres_tools import insert_and_get_row_lifespan
25-
26-
27-
# WARNING: DO NOT use UserDict is already in https://docs.python.org/3/library/collections.html#collections.UserDictclass UserRowDict(TypedDict):
28-
# NOTE: this is modified dict version of packages/postgres-database/src/simcore_postgres_database/models/users.py for testing purposes
29-
class _UserInfoDictRequired(TypedDict, total=True):
30-
id: int
31-
name: str
32-
email: str
33-
primary_gid: str
34-
raw_password: str
35-
status: UserStatus
36-
role: UserRole
37-
38-
39-
class UserInfoDict(_UserInfoDictRequired, total=False):
40-
created_at: datetime
41-
password_hash: str
42-
first_name: str
43-
last_name: str
44-
phone: str
45-
17+
from .faker_factories import DEFAULT_FAKER
18+
from .webserver_users import NewUser, UserInfoDict, _create_account_in_db
4619

4720
TEST_MARKS = re.compile(r"TEST (\w+):(.*)")
4821

@@ -65,89 +38,6 @@ def parse_link(text):
6538
return URL(link).path
6639

6740

68-
async def _create_user_in_db(
69-
sqlalchemy_async_engine: AsyncEngine,
70-
exit_stack: contextlib.AsyncExitStack,
71-
data: dict | None = None,
72-
) -> UserInfoDict:
73-
74-
# create fake
75-
data = data or {}
76-
data.setdefault("status", UserStatus.ACTIVE.name)
77-
data.setdefault("role", UserRole.USER.name)
78-
79-
raw_password = DEFAULT_TEST_PASSWORD
80-
81-
# inject in db
82-
user = await exit_stack.enter_async_context(
83-
insert_and_get_row_lifespan( # pylint:disable=contextmanager-generator-missing-cleanup
84-
sqlalchemy_async_engine,
85-
table=users_table,
86-
values=random_user(password=raw_password, **data),
87-
pk_col=users_table.c.id,
88-
)
89-
)
90-
assert "first_name" in user
91-
assert "last_name" in user
92-
93-
return UserInfoDict(
94-
# required
95-
# - in db
96-
id=user["id"],
97-
name=user["name"],
98-
email=user["email"],
99-
primary_gid=user["primary_gid"],
100-
status=(
101-
UserStatus(user["status"])
102-
if not isinstance(user["status"], UserStatus)
103-
else user["status"]
104-
),
105-
role=(
106-
UserRole(user["role"])
107-
if not isinstance(user["role"], UserRole)
108-
else user["role"]
109-
),
110-
# optional
111-
# - in db
112-
created_at=(
113-
user["created_at"]
114-
if isinstance(user["created_at"], datetime)
115-
else datetime.fromisoformat(user["created_at"])
116-
),
117-
password_hash=user["password_hash"],
118-
first_name=user["first_name"],
119-
last_name=user["last_name"],
120-
phone=user["phone"],
121-
# extras
122-
raw_password=raw_password,
123-
)
124-
125-
126-
async def _register_user_in_default_product(app: web.Application, user_id: UserID):
127-
products = list_products(app)
128-
assert products
129-
product_name = products[0].name
130-
131-
return await groups_service.auto_add_user_to_product_group(
132-
app, user_id, product_name=product_name
133-
)
134-
135-
136-
async def _create_account_in_db(
137-
app: web.Application,
138-
exit_stack: contextlib.AsyncExitStack,
139-
user_data: dict[str, Any] | None = None,
140-
) -> UserInfoDict:
141-
# users, groups in db
142-
user = await _create_user_in_db(
143-
get_asyncpg_engine(app), exit_stack=exit_stack, data=user_data
144-
)
145-
146-
# user has default product
147-
await _register_user_in_default_product(app, user_id=user["id"])
148-
return user
149-
150-
15141
async def log_client_in(
15242
client: TestClient,
15343
user_data: dict[str, Any] | None = None,
@@ -178,30 +68,6 @@ async def log_client_in(
17868
return user
17969

18070

181-
class NewUser:
182-
def __init__(
183-
self,
184-
user_data: dict[str, Any] | None = None,
185-
app: web.Application | None = None,
186-
):
187-
self.user_data = user_data
188-
self.user = None
189-
190-
assert app
191-
self.app = app
192-
193-
self.exit_stack = contextlib.AsyncExitStack()
194-
195-
async def __aenter__(self) -> UserInfoDict:
196-
self.user = await _create_account_in_db(
197-
self.app, self.exit_stack, self.user_data
198-
)
199-
return self.user
200-
201-
async def __aexit__(self, *args):
202-
await self.exit_stack.aclose()
203-
204-
20571
class LoggedUser(NewUser):
20672
def __init__(self, client: TestClient, user_data=None, *, check_if_succeeds=True):
20773
super().__init__(user_data, client.app)
@@ -266,13 +132,12 @@ def __init__(
266132
self.confirmation = None
267133
self.trial_days = trial_days
268134
self.extra_credits_in_usd = extra_credits_in_usd
135+
self.db = get_plugin_storage(self.app)
269136

270137
async def __aenter__(self) -> "NewInvitation":
271138
# creates host user
272139
assert self.client.app
273-
self.user = await _create_user_in_db(
274-
self.client.app, self.exit_stack, self.user_data
275-
)
140+
self.user = await super().__aenter__()
276141

277142
self.confirmation = await create_invitation_token(
278143
self.db,
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import contextlib
2+
from datetime import datetime
3+
from typing import Any, TypedDict
4+
5+
from aiohttp import web
6+
from models_library.users import UserID
7+
from servicelib.aiohttp import status
8+
from simcore_postgres_database.models.users import users as users_table
9+
from simcore_service_webserver.db.models import UserRole, UserStatus
10+
from simcore_service_webserver.db.plugin import get_asyncpg_engine
11+
from simcore_service_webserver.groups import api as groups_service
12+
from simcore_service_webserver.products.products_service import list_products
13+
from sqlalchemy.ext.asyncio import AsyncEngine
14+
15+
from .faker_factories import DEFAULT_TEST_PASSWORD, random_user
16+
from .postgres_tools import insert_and_get_row_lifespan
17+
18+
19+
# WARNING: DO NOT use UserDict is already in https://docs.python.org/3/library/collections.html#collections.UserDictclass UserRowDict(TypedDict):
20+
# NOTE: this is modified dict version of packages/postgres-database/src/simcore_postgres_database/models/users.py for testing purposes
21+
class _UserInfoDictRequired(TypedDict, total=True):
22+
id: int
23+
name: str
24+
email: str
25+
primary_gid: str
26+
raw_password: str
27+
status: UserStatus
28+
role: UserRole
29+
30+
31+
class UserInfoDict(_UserInfoDictRequired, total=False):
32+
created_at: datetime
33+
password_hash: str
34+
first_name: str
35+
last_name: str
36+
phone: str
37+
38+
39+
async def _create_user_in_db(
40+
sqlalchemy_async_engine: AsyncEngine,
41+
exit_stack: contextlib.AsyncExitStack,
42+
data: dict | None = None,
43+
) -> UserInfoDict:
44+
45+
# create fake
46+
data = data or {}
47+
data.setdefault("status", UserStatus.ACTIVE.name)
48+
data.setdefault("role", UserRole.USER.name)
49+
50+
raw_password = DEFAULT_TEST_PASSWORD
51+
52+
# inject in db
53+
user = await exit_stack.enter_async_context(
54+
insert_and_get_row_lifespan( # pylint:disable=contextmanager-generator-missing-cleanup
55+
sqlalchemy_async_engine,
56+
table=users_table,
57+
values=random_user(password=raw_password, **data),
58+
pk_col=users_table.c.id,
59+
)
60+
)
61+
assert "first_name" in user
62+
assert "last_name" in user
63+
64+
return UserInfoDict(
65+
# required
66+
# - in db
67+
id=user["id"],
68+
name=user["name"],
69+
email=user["email"],
70+
primary_gid=user["primary_gid"],
71+
status=(
72+
UserStatus(user["status"])
73+
if not isinstance(user["status"], UserStatus)
74+
else user["status"]
75+
),
76+
role=(
77+
UserRole(user["role"])
78+
if not isinstance(user["role"], UserRole)
79+
else user["role"]
80+
),
81+
# optional
82+
# - in db
83+
created_at=(
84+
user["created_at"]
85+
if isinstance(user["created_at"], datetime)
86+
else datetime.fromisoformat(user["created_at"])
87+
),
88+
password_hash=user["password_hash"],
89+
first_name=user["first_name"],
90+
last_name=user["last_name"],
91+
phone=user["phone"],
92+
# extras
93+
raw_password=raw_password,
94+
)
95+
96+
97+
async def _register_user_in_default_product(app: web.Application, user_id: UserID):
98+
products = list_products(app)
99+
assert products
100+
product_name = products[0].name
101+
102+
return await groups_service.auto_add_user_to_product_group(
103+
app, user_id, product_name=product_name
104+
)
105+
106+
107+
async def _create_account_in_db(
108+
app: web.Application,
109+
exit_stack: contextlib.AsyncExitStack,
110+
user_data: dict[str, Any] | None = None,
111+
) -> UserInfoDict:
112+
# users, groups in db
113+
user = await _create_user_in_db(
114+
get_asyncpg_engine(app), exit_stack=exit_stack, data=user_data
115+
)
116+
117+
# user has default product
118+
await _register_user_in_default_product(app, user_id=user["id"])
119+
return user
120+
121+
122+
class NewUser:
123+
def __init__(
124+
self,
125+
user_data: dict[str, Any] | None = None,
126+
app: web.Application | None = None,
127+
):
128+
self.user_data = user_data
129+
self.user = None
130+
131+
assert app
132+
self.app = app
133+
134+
self.exit_stack = contextlib.AsyncExitStack()
135+
136+
async def __aenter__(self) -> UserInfoDict:
137+
self.user = await _create_account_in_db(
138+
self.app, self.exit_stack, self.user_data
139+
)
140+
return self.user
141+
142+
async def __aexit__(self, *args):
143+
await self.exit_stack.aclose()

services/web/server/src/simcore_service_webserver/application.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
from .groups.plugin import setup_groups
2929
from .invitations.plugin import setup_invitations
3030
from .licenses.plugin import setup_licenses
31-
from .login.plugin import setup_login, setup_login_auth
31+
from .login.plugin import setup_login
32+
from .login_auth.plugin import setup_login_auth
3233
from .long_running_tasks import setup_long_running_tasks
3334
from .notifications.plugin import setup_notifications
3435
from .payments.plugin import setup_payments

services/web/server/src/simcore_service_webserver/login/plugin.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
import asyncpg
55
from aiohttp import web
66
from pydantic import ValidationError
7-
from servicelib.aiohttp.application_setup import ModuleCategory, app_module_setup
7+
from servicelib.aiohttp.application_setup import (
8+
ModuleCategory,
9+
app_module_setup,
10+
ensure_single_setup,
11+
)
812
from settings_library.email import SMTPSettings
913
from settings_library.postgres import PostgresSettings
1014

@@ -18,7 +22,7 @@
1822
from ..email.plugin import setup_email
1923
from ..email.settings import get_plugin_settings as get_email_plugin_settings
2024
from ..invitations.plugin import setup_invitations
21-
from ..login_auth.plugin import ensure_single_setup, setup_login_auth
25+
from ..login_auth.plugin import setup_login_auth
2226
from ..products import products_service
2327
from ..products.models import ProductName
2428
from ..products.plugin import setup_products

services/web/server/tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# pylint: disable=unused-variable
55

66
import asyncio
7+
import contextlib
78
import json
89
import logging
910
import random
@@ -88,6 +89,13 @@
8889
]
8990

9091

92+
@pytest.fixture
93+
async def exit_stack() -> AsyncIterator[contextlib.AsyncExitStack]:
94+
"""Provides an AsyncExitStack that gets cleaned up after each test"""
95+
async with contextlib.AsyncExitStack() as stack:
96+
yield stack
97+
98+
9199
@pytest.fixture(scope="session")
92100
def package_dir() -> Path:
93101
"""osparc-simcore installed directory"""

0 commit comments

Comments
 (0)