Skip to content

Commit fc1cd56

Browse files
committed
search by email, gid or uname
1 parent 6eebe5f commit fc1cd56

File tree

2 files changed

+185
-46
lines changed

2 files changed

+185
-46
lines changed

services/web/server/src/simcore_service_webserver/users/_accounts_repository.py

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,12 @@ async def search_merged_pre_and_registered_users(
272272
engine: AsyncEngine,
273273
connection: AsyncConnection | None = None,
274274
*,
275-
email_like: str,
275+
email_like: str | None = None,
276+
user_name_like: str | None = None,
277+
primary_group_id: int | None = None,
276278
product_name: ProductName | None = None,
277279
) -> list[Row]:
280+
"""Searches and merges users from both users and pre-registration tables"""
278281
users_alias = sa.alias(users, name="users_alias")
279282

280283
invited_by = (
@@ -285,18 +288,6 @@ async def search_merged_pre_and_registered_users(
285288
.label("invited_by")
286289
)
287290

288-
reviewer_alias = sa.alias(users, name="reviewer_alias")
289-
account_request_reviewed_by_username = (
290-
sa.select(
291-
reviewer_alias.c.name,
292-
)
293-
.where(
294-
users_pre_registration_details.c.account_request_reviewed_by
295-
== reviewer_alias.c.id
296-
)
297-
.label("account_request_reviewed_by_username")
298-
)
299-
300291
async with pass_or_acquire_connection(engine, connection) as conn:
301292
columns = (
302293
users_pre_registration_details.c.id,
@@ -317,39 +308,69 @@ async def search_merged_pre_and_registered_users(
317308
users_pre_registration_details.c.user_id,
318309
users_pre_registration_details.c.extras,
319310
users_pre_registration_details.c.account_request_status,
320-
account_request_reviewed_by_username,
311+
users_pre_registration_details.c.account_request_reviewed_by,
321312
users_pre_registration_details.c.account_request_reviewed_at,
322313
users.c.status,
323314
invited_by,
324315
users_pre_registration_details.c.created,
325316
)
326317

318+
# Build where conditions for left outer join (pre-registered users)
319+
left_where_conditions = []
320+
if email_like is not None:
321+
left_where_conditions.append(
322+
users_pre_registration_details.c.pre_email.like(email_like)
323+
)
324+
325+
# Build join condition
327326
join_condition = users.c.id == users_pre_registration_details.c.user_id
328327
if product_name:
329328
join_condition = join_condition & (
330329
users_pre_registration_details.c.product_name == product_name
331330
)
332331

333-
left_outer_join = (
334-
sa.select(*columns)
335-
.select_from(
336-
users_pre_registration_details.outerjoin(users, join_condition)
337-
)
338-
.where(users_pre_registration_details.c.pre_email.like(email_like))
332+
# Build where conditions for right outer join (registered users)
333+
right_where_conditions = []
334+
if email_like is not None:
335+
right_where_conditions.append(users.c.email.like(email_like))
336+
if user_name_like is not None:
337+
right_where_conditions.append(users.c.name.like(user_name_like))
338+
if primary_group_id is not None:
339+
# Join with user_to_groups to filter by primary group
340+
right_where_conditions.append(users.c.primary_gid == primary_group_id)
341+
342+
# Left outer join query (pre-registered users)
343+
left_outer_join = sa.select(*columns).select_from(
344+
users_pre_registration_details.outerjoin(users, join_condition)
339345
)
340-
right_outer_join = (
341-
sa.select(*columns)
342-
.select_from(
343-
users.outerjoin(
344-
users_pre_registration_details,
345-
join_condition,
346-
)
346+
if left_where_conditions:
347+
left_outer_join = left_outer_join.where(sa.and_(*left_where_conditions))
348+
349+
# Right outer join query (registered users)
350+
right_outer_join = sa.select(*columns).select_from(
351+
users.outerjoin(
352+
users_pre_registration_details,
353+
join_condition,
347354
)
348-
.where(users.c.email.like(email_like))
349355
)
356+
if right_where_conditions:
357+
right_outer_join = right_outer_join.where(sa.and_(*right_where_conditions))
350358

351-
result = await conn.stream(sa.union(left_outer_join, right_outer_join))
352-
return [row async for row in result]
359+
# Only execute queries if we have meaningful search criteria
360+
queries = []
361+
if left_where_conditions:
362+
queries.append(left_outer_join)
363+
if right_where_conditions:
364+
queries.append(right_outer_join)
365+
366+
if not queries:
367+
# No search criteria provided, return empty result
368+
return []
369+
370+
final_query = queries[0] if len(queries) == 1 else sa.union(*queries)
371+
372+
result = await conn.execute(final_query)
373+
return result.fetchall()
353374

354375

355376
async def list_merged_pre_and_registered_users(
@@ -400,18 +421,6 @@ async def list_merged_pre_and_registered_users(
400421

401422
# Query for pre-registered users that are not yet in the users table
402423
# We need to left join with users to identify if the pre-registered user is already in the system
403-
404-
reviewer_alias = sa.alias(users, name="reviewer_alias")
405-
account_request_reviewed_by_username = (
406-
sa.select(
407-
reviewer_alias.c.name,
408-
)
409-
.where(
410-
users_pre_registration_details.c.account_request_reviewed_by
411-
== reviewer_alias.c.id
412-
)
413-
.label("account_request_reviewed_by_username")
414-
)
415424
pre_reg_query = (
416425
sa.select(
417426
users_pre_registration_details.c.id,
@@ -429,7 +438,7 @@ async def list_merged_pre_and_registered_users(
429438
users_pre_registration_details.c.extras,
430439
users_pre_registration_details.c.created,
431440
users_pre_registration_details.c.account_request_status,
432-
account_request_reviewed_by_username,
441+
users_pre_registration_details.c.account_request_reviewed_by,
433442
users_pre_registration_details.c.account_request_reviewed_at,
434443
users.c.id.label("user_id"),
435444
users.c.name.label("user_name"),
@@ -453,7 +462,7 @@ async def list_merged_pre_and_registered_users(
453462
users.c.email,
454463
users.c.first_name,
455464
users.c.last_name,
456-
users.c.phone, # verified phone!
465+
users.c.phone,
457466
sa.literal(None).label("institution"),
458467
sa.literal(None).label("address"),
459468
sa.literal(None).label("city"),

services/web/server/tests/unit/with_dbs/03/users/test_users_accounts_repository.py

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from aiohttp import web
1313
from common_library.users_enums import AccountRequestStatus
1414
from models_library.products import ProductName
15+
from models_library.users import UserID
1516
from simcore_postgres_database.models.users_details import (
1617
users_pre_registration_details,
1718
)
@@ -477,9 +478,9 @@ async def test_create_pre_registration_with_existing_user_linking(
477478
class MixedUserTestData:
478479
"""Test data for user pre-registration tests with mixed states."""
479480

480-
created_by_user_id: str
481+
created_by_user_id: UserID
481482
product_owner_email: str
482-
product_owner_id: str
483+
product_owner_id: UserID
483484
pre_reg_email: str
484485
pre_reg_id: int
485486
owner_pre_reg_id: int
@@ -829,3 +830,132 @@ async def test_list_merged_users_pagination(
829830
assert not set(page1_emails).intersection(
830831
set(page2_emails)
831832
), "Pages should have different users"
833+
834+
835+
@pytest.mark.parametrize(
836+
"email_pattern,expected_count",
837+
[
838+
("%pre.registered%", 1), # Valid: matches pre-registered user
839+
("%nonexistent%", 0), # Invalid: no matches
840+
],
841+
)
842+
async def test_search_merged_users_by_email(
843+
app: web.Application,
844+
product_name: ProductName,
845+
mixed_user_data: MixedUserTestData,
846+
email_pattern: str,
847+
expected_count: int,
848+
):
849+
"""Test searching merged users by email pattern."""
850+
asyncpg_engine = get_asyncpg_engine(app)
851+
852+
# Act
853+
rows = await _accounts_repository.search_merged_pre_and_registered_users(
854+
asyncpg_engine,
855+
email_like=email_pattern,
856+
product_name=product_name,
857+
)
858+
859+
# Assert
860+
assert len(rows) == expected_count
861+
862+
if expected_count > 0:
863+
row = rows[0]
864+
assert row.pre_email == mixed_user_data.pre_reg_email
865+
assert row.pre_first_name == "Pre-Registered"
866+
assert row.pre_last_name == "Only"
867+
assert row.institution == "Pre-Reg Institution"
868+
869+
870+
@pytest.mark.parametrize(
871+
"use_valid_username,expected_count",
872+
[
873+
(True, 1), # Valid: use actual product owner username
874+
(False, 0), # Invalid: use non-existent username
875+
],
876+
)
877+
async def test_search_merged_users_by_username(
878+
app: web.Application,
879+
product_name: ProductName,
880+
product_owner_user: dict[str, Any],
881+
use_valid_username: bool,
882+
expected_count: int,
883+
):
884+
"""Test searching merged users by username pattern."""
885+
asyncpg_engine = get_asyncpg_engine(app)
886+
887+
# Arrange
888+
username_pattern = (
889+
f"{product_owner_user['name']}"
890+
if use_valid_username
891+
else "%nonexistent_username%"
892+
)
893+
894+
# Act
895+
rows = await _accounts_repository.search_merged_pre_and_registered_users(
896+
asyncpg_engine,
897+
user_name_like=username_pattern,
898+
product_name=product_name,
899+
)
900+
901+
# Assert
902+
assert len(rows) >= expected_count
903+
904+
if expected_count > 0:
905+
# Find the product owner in rows
906+
found_user = next(
907+
(row for row in rows if row.email == product_owner_user["email"]),
908+
None,
909+
)
910+
assert found_user is not None
911+
assert found_user.first_name == product_owner_user["first_name"]
912+
assert found_user.last_name == product_owner_user["last_name"]
913+
914+
915+
@pytest.mark.parametrize(
916+
"use_valid_group_id,expected_count",
917+
[
918+
(True, 1), # Valid: use actual product owner primary group ID
919+
(False, 0), # Invalid: use non-existent group ID
920+
],
921+
)
922+
async def test_search_merged_users_by_primary_group_id(
923+
app: web.Application,
924+
product_name: ProductName,
925+
product_owner_user: dict[str, Any],
926+
use_valid_group_id: bool,
927+
expected_count: int,
928+
):
929+
"""Test searching merged users by primary group ID."""
930+
asyncpg_engine = get_asyncpg_engine(app)
931+
932+
# Arrange
933+
primary_group_id = (
934+
product_owner_user["primary_gid"]
935+
if use_valid_group_id
936+
else 99999 # Non-existent group ID
937+
)
938+
939+
# Act
940+
results = await _accounts_repository.search_merged_pre_and_registered_users(
941+
asyncpg_engine,
942+
primary_group_id=primary_group_id,
943+
product_name=product_name,
944+
)
945+
946+
# Assert
947+
assert len(results) >= expected_count
948+
949+
if expected_count > 0:
950+
# Find the product owner in results
951+
found_user = next(
952+
(
953+
result
954+
for result in results
955+
if result.email == product_owner_user["email"]
956+
),
957+
None,
958+
)
959+
assert found_user is not None
960+
assert found_user.first_name == product_owner_user["first_name"]
961+
assert found_user.last_name == product_owner_user["last_name"]

0 commit comments

Comments
 (0)