Skip to content

Commit d82ee28

Browse files
committed
test coverage
1 parent a8ba29a commit d82ee28

File tree

2 files changed

+93
-38
lines changed

2 files changed

+93
-38
lines changed

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

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,38 @@
2424
_logger = logging.getLogger(__name__)
2525

2626

27-
async def validate_confirmation_code(
28-
code: str, db: AsyncpgStorage, cfg: LoginOptions
29-
) -> ConfirmationTokenDict | None:
30-
"""
31-
Returns None if validation fails
32-
"""
33-
assert not code.startswith("***"), "forgot .get_secret_value()??" # nosec
27+
async def get_or_create_confirmation_without_data(
28+
cfg: LoginOptions,
29+
db: AsyncpgStorage,
30+
user_id: UserID,
31+
action: ActionLiteralStr,
32+
) -> ConfirmationTokenDict:
3433

3534
confirmation: ConfirmationTokenDict | None = await db.get_confirmation(
36-
{"code": code}
35+
{"user": {"id": user_id}, "action": action}
3736
)
38-
if confirmation and is_confirmation_expired(cfg, confirmation):
37+
38+
if confirmation is not None and is_confirmation_expired(cfg, confirmation):
3939
await db.delete_confirmation(confirmation)
4040
_logger.warning(
4141
"Used expired token [%s]. Deleted from confirmations table.",
4242
confirmation,
4343
)
44-
return None
44+
confirmation = None
45+
46+
if confirmation is None:
47+
confirmation = await db.create_confirmation(user_id, action=action)
48+
4549
return confirmation
4650

4751

52+
def get_expiration_date(
53+
cfg: LoginOptions, confirmation: ConfirmationTokenDict
54+
) -> datetime:
55+
lifetime = cfg.get_confirmation_lifetime(confirmation["action"])
56+
return confirmation["created_at"] + lifetime
57+
58+
4859
def _url_for_confirmation(app: web.Application, code: str) -> URL:
4960
# NOTE: this is in a query parameter, and can contain ? for example.
5061
safe_code = quote(code, safe="")
@@ -58,39 +69,28 @@ def make_confirmation_link(
5869
return f"{request.scheme}://{request.host}{link}"
5970

6071

61-
def get_expiration_date(
62-
cfg: LoginOptions, confirmation: ConfirmationTokenDict
63-
) -> datetime:
72+
def is_confirmation_expired(cfg: LoginOptions, confirmation: ConfirmationTokenDict):
73+
age = datetime.utcnow() - confirmation["created_at"]
6474
lifetime = cfg.get_confirmation_lifetime(confirmation["action"])
65-
return confirmation["created_at"] + lifetime
75+
return age > lifetime
6676

6777

68-
async def get_or_create_confirmation_without_data(
69-
cfg: LoginOptions,
70-
db: AsyncpgStorage,
71-
user_id: UserID,
72-
action: ActionLiteralStr,
73-
) -> ConfirmationTokenDict:
78+
async def validate_confirmation_code(
79+
code: str, db: AsyncpgStorage, cfg: LoginOptions
80+
) -> ConfirmationTokenDict | None:
81+
"""
82+
Returns None if validation fails
83+
"""
84+
assert not code.startswith("***"), "forgot .get_secret_value()??" # nosec
7485

7586
confirmation: ConfirmationTokenDict | None = await db.get_confirmation(
76-
{"user": {"id": user_id}, "action": action}
87+
{"code": code}
7788
)
78-
79-
if confirmation is not None and is_confirmation_expired(cfg, confirmation):
89+
if confirmation and is_confirmation_expired(cfg, confirmation):
8090
await db.delete_confirmation(confirmation)
8191
_logger.warning(
8292
"Used expired token [%s]. Deleted from confirmations table.",
8393
confirmation,
8494
)
85-
confirmation = None
86-
87-
if confirmation is None:
88-
confirmation = await db.create_confirmation(user_id, action=action)
89-
95+
return None
9096
return confirmation
91-
92-
93-
def is_confirmation_expired(cfg: LoginOptions, confirmation: ConfirmationTokenDict):
94-
age = datetime.utcnow() - confirmation["created_at"]
95-
lifetime = cfg.get_confirmation_lifetime(confirmation["action"])
96-
return age > lifetime

services/web/server/tests/unit/with_dbs/03/login/test_login_confirmation_service.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from datetime import timedelta
2+
13
from aiohttp.test_utils import make_mocked_request
24
from aiohttp.web import Application
3-
from models_library.users import UserID
5+
from pytest_simcore.helpers.webserver_login import UserInfoDict
46
from simcore_service_webserver.login._confirmation_service import (
57
get_or_create_confirmation_without_data,
68
is_confirmation_expired,
@@ -12,9 +14,10 @@
1214

1315

1416
async def test_confirmation_token_workflow(
15-
db: AsyncpgStorage, login_options: LoginOptions, user_id: UserID
17+
db: AsyncpgStorage, login_options: LoginOptions, registered_user: UserInfoDict
1618
):
1719
# Step 1: Create a new confirmation token
20+
user_id = registered_user["id"]
1821
action = "RESET_PASSWORD"
1922
confirmation = await get_or_create_confirmation_without_data(
2023
login_options, db, user_id=user_id, action=action
@@ -42,13 +45,65 @@ async def test_confirmation_token_workflow(
4245
"/auth/confirmation/{code}", lambda request: None, name="auth_confirmation"
4346
)
4447
request = make_mocked_request(
45-
"GET", "/auth/confirmation/{code}", app=app, headers={"Host": "example.com"}
48+
"GET",
49+
"http://example.com/auth/confirmation/{code}",
50+
app=app,
51+
headers={"Host": "example.com"},
4652
)
47-
request.scheme = "http"
4853

4954
# Create confirmation link
5055
confirmation_link = make_confirmation_link(request, confirmation)
5156

5257
# Assertions
5358
assert confirmation_link.startswith("http://example.com/auth/confirmation/")
5459
assert confirmation["code"] in confirmation_link
60+
61+
62+
async def test_expired_confirmation_token(
63+
db: AsyncpgStorage, login_options: LoginOptions, registered_user: UserInfoDict
64+
):
65+
user_id = registered_user["id"]
66+
action = "CHANGE_EMAIL"
67+
68+
# Create a brand new confirmation token
69+
confirmation_1 = await get_or_create_confirmation_without_data(
70+
login_options, db, user_id=user_id, action=action
71+
)
72+
73+
assert confirmation_1 is not None
74+
assert confirmation_1["user_id"] == user_id
75+
assert confirmation_1["action"] == action
76+
77+
# Check that the token is not expired
78+
assert not is_confirmation_expired(login_options, confirmation_1)
79+
80+
confirmation_2 = await get_or_create_confirmation_without_data(
81+
login_options, db, user_id=user_id, action=action
82+
)
83+
84+
assert confirmation_2 == confirmation_1
85+
86+
# Enforce ALL EXPIRED
87+
login_options.CHANGE_EMAIL_CONFIRMATION_LIFETIME = 0
88+
assert login_options.get_confirmation_lifetime(action) == timedelta(seconds=0)
89+
90+
confirmation_3 = await get_or_create_confirmation_without_data(
91+
login_options, db, user_id=user_id, action=action
92+
)
93+
94+
# when expired, it gets renewed
95+
assert confirmation_3 != confirmation_1
96+
97+
# now all have expired
98+
assert (
99+
await validate_confirmation_code(confirmation_1["code"], db, login_options)
100+
is None
101+
)
102+
assert (
103+
await validate_confirmation_code(confirmation_2["code"], db, login_options)
104+
is None
105+
)
106+
assert (
107+
await validate_confirmation_code(confirmation_3["code"], db, login_options)
108+
is None
109+
)

0 commit comments

Comments
 (0)