Skip to content

Commit 48e3781

Browse files
committed
drafts tests for listing
1 parent abce752 commit 48e3781

File tree

3 files changed

+375
-0
lines changed

3 files changed

+375
-0
lines changed

services/catalog/src/simcore_service_catalog/repository/_services_sql.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,3 +436,79 @@ def get_service_history_stmt(
436436
.select_from(history_subquery)
437437
.group_by(history_subquery.c.key)
438438
)
439+
440+
441+
def all_services_total_count_stmt(
442+
*,
443+
product_name: ProductName,
444+
user_id: UserID,
445+
access_rights: AccessRightsClauses,
446+
filters: ServiceDBFilters | None = None,
447+
) -> sa.sql.Select:
448+
"""Statement to count all services"""
449+
stmt = (
450+
sa.select(sa.func.count())
451+
.select_from(
452+
services_meta_data.join(
453+
services_access_rights,
454+
(services_meta_data.c.key == services_access_rights.c.key)
455+
& (services_meta_data.c.version == services_access_rights.c.version),
456+
).join(
457+
user_to_groups,
458+
(user_to_groups.c.gid == services_access_rights.c.gid),
459+
)
460+
)
461+
.where(
462+
(services_access_rights.c.product_name == product_name)
463+
& (user_to_groups.c.uid == user_id)
464+
& access_rights
465+
)
466+
)
467+
468+
if filters:
469+
stmt = apply_services_filters(stmt, filters)
470+
471+
return stmt
472+
473+
474+
def list_all_services_stmt(
475+
*,
476+
product_name: ProductName,
477+
user_id: UserID,
478+
access_rights: AccessRightsClauses,
479+
limit: int | None = None,
480+
offset: int | None = None,
481+
filters: ServiceDBFilters | None = None,
482+
) -> sa.sql.Select:
483+
"""Statement to list all services with pagination"""
484+
stmt = (
485+
sa.select(*SERVICES_META_DATA_COLS)
486+
.select_from(
487+
services_meta_data.join(
488+
services_access_rights,
489+
(services_meta_data.c.key == services_access_rights.c.key)
490+
& (services_meta_data.c.version == services_access_rights.c.version),
491+
).join(
492+
user_to_groups,
493+
(user_to_groups.c.gid == services_access_rights.c.gid),
494+
)
495+
)
496+
.where(
497+
(services_access_rights.c.product_name == product_name)
498+
& (user_to_groups.c.uid == user_id)
499+
& access_rights
500+
)
501+
.order_by(
502+
services_meta_data.c.key, sa.desc(by_version(services_meta_data.c.version))
503+
)
504+
)
505+
506+
if filters:
507+
stmt = apply_services_filters(stmt, filters)
508+
509+
if offset is not None:
510+
stmt = stmt.offset(offset)
511+
if limit is not None:
512+
stmt = stmt.limit(limit)
513+
514+
return stmt

services/catalog/src/simcore_service_catalog/repository/services.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,46 @@ async def get_service_with_history(
376376
)
377377
return None
378378

379+
async def list_all_services(
380+
self,
381+
*,
382+
# access-rights
383+
product_name: ProductName,
384+
user_id: UserID,
385+
# list args: pagination
386+
pagination_limit: int | None = None,
387+
pagination_offset: int | None = None,
388+
filters: ServiceDBFilters | None = None,
389+
) -> tuple[PositiveInt, list[ServiceMetaDataDBGet]]:
390+
# get page count
391+
stmt_total = _services_sql.all_services_total_count_stmt(
392+
product_name=product_name,
393+
user_id=user_id,
394+
access_rights=AccessRightsClauses.can_read,
395+
filters=filters,
396+
)
397+
398+
# get page content
399+
stmt_page = _services_sql.list_all_services_stmt(
400+
product_name=product_name,
401+
user_id=user_id,
402+
access_rights=AccessRightsClauses.can_read,
403+
limit=pagination_limit,
404+
offset=pagination_offset,
405+
filters=filters,
406+
)
407+
408+
async with self.db_engine.connect() as conn:
409+
result = await conn.execute(stmt_total)
410+
total_count = result.scalar() or 0
411+
412+
items_page = [
413+
ServiceMetaDataDBGet.model_validate(row)
414+
async for row in await conn.stream(stmt_page)
415+
]
416+
417+
return (total_count, items_page)
418+
379419
async def list_latest_services(
380420
self,
381421
*,

services/catalog/tests/unit/with_dbs/test_repositories.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,3 +900,262 @@ async def test_list_services_from_published_templates_with_invalid_service(
900900
"service {'key': 'simcore/services/dynamic/invalid-service', 'version': 'invalid'} could not be validated"
901901
in caplog.text
902902
)
903+
904+
905+
async def test_compare_list_all_and_latest_services(
906+
target_product: ProductName,
907+
create_fake_service_data: CreateFakeServiceDataCallable,
908+
services_db_tables_injector: Callable,
909+
services_repo: ServicesRepository,
910+
user_id: UserID,
911+
):
912+
# Setup: Create multiple versions of the same service and a few distinct services
913+
service_data: list[tuple] = []
914+
915+
# Service 1 with multiple versions
916+
service_key_1 = "simcore/services/dynamic/multi-version"
917+
service_versions_1 = ["1.0.0", "1.1.0", "2.0.0"]
918+
service_data.extend(
919+
[
920+
create_fake_service_data(
921+
service_key_1,
922+
version_,
923+
team_access=None,
924+
everyone_access=None,
925+
product=target_product,
926+
)
927+
for version_ in service_versions_1
928+
]
929+
)
930+
931+
# Service 2 with single version
932+
service_key_2 = "simcore/services/dynamic/single-version"
933+
service_data.append(
934+
create_fake_service_data(
935+
service_key_2,
936+
"1.0.0",
937+
team_access=None,
938+
everyone_access=None,
939+
product=target_product,
940+
)
941+
)
942+
943+
# Service 3 with computational type
944+
service_key_3 = "simcore/services/comp/computational-service"
945+
service_versions_3 = ["0.5.0", "1.0.0"]
946+
service_data.extend(
947+
[
948+
create_fake_service_data(
949+
service_key_3,
950+
version_,
951+
team_access=None,
952+
everyone_access=None,
953+
product=target_product,
954+
)
955+
for version_ in service_versions_3
956+
]
957+
)
958+
959+
await services_db_tables_injector(service_data)
960+
961+
# Test 1: Compare all services vs latest without filters
962+
total_all, all_services = await services_repo.list_all_services(
963+
product_name=target_product, user_id=user_id
964+
)
965+
total_latest, latest_services = await services_repo.list_latest_services(
966+
product_name=target_product, user_id=user_id
967+
)
968+
969+
# Verify counts
970+
# All services should be 6 (3 versions of service 1, 1 of service 2, 2 of service 3)
971+
assert total_all == 6
972+
# Latest services should be 3 (one latest for each distinct service key)
973+
assert total_latest == 3
974+
975+
# Verify latest services are contained in all services
976+
latest_key_versions = {(s.key, s.version) for s in latest_services}
977+
all_key_versions = {(s.key, s.version) for s in all_services}
978+
assert latest_key_versions.issubset(all_key_versions)
979+
980+
# Verify latest versions are correct
981+
latest_versions_by_key = {s.key: s.version for s in latest_services}
982+
assert latest_versions_by_key[service_key_1] == "2.0.0"
983+
assert latest_versions_by_key[service_key_2] == "1.0.0"
984+
assert latest_versions_by_key[service_key_3] == "1.0.0"
985+
986+
# Test 2: Using service_type filter to get only dynamic services
987+
filters = ServiceDBFilters(service_type=ServiceType.DYNAMIC)
988+
989+
total_all_filtered, all_services_filtered = await services_repo.list_all_services(
990+
product_name=target_product, user_id=user_id, filters=filters
991+
)
992+
total_latest_filtered, latest_services_filtered = (
993+
await services_repo.list_latest_services(
994+
product_name=target_product, user_id=user_id, filters=filters
995+
)
996+
)
997+
998+
# Verify counts with filter
999+
assert total_all_filtered == 4 # 3 versions of service 1, 1 of service 2
1000+
assert total_latest_filtered == 2 # 1 latest each for service 1 and 2
1001+
1002+
# Verify service types are correct after filtering
1003+
assert all(
1004+
s.key.startswith(DYNAMIC_SERVICE_KEY_PREFIX) for s in all_services_filtered
1005+
)
1006+
assert all(
1007+
s.key.startswith(DYNAMIC_SERVICE_KEY_PREFIX) for s in latest_services_filtered
1008+
)
1009+
1010+
# Verify latest versions are correct
1011+
latest_versions_by_key = {s.key: s.version for s in latest_services_filtered}
1012+
assert latest_versions_by_key[service_key_1] == "2.0.0"
1013+
assert latest_versions_by_key[service_key_2] == "1.0.0"
1014+
assert service_key_3 not in latest_versions_by_key # Filtered out
1015+
1016+
# Test 3: Using service_key_pattern to find specific service
1017+
filters = ServiceDBFilters(service_key_pattern="*/multi-*")
1018+
1019+
total_all_filtered, all_services_filtered = await services_repo.list_all_services(
1020+
product_name=target_product, user_id=user_id, filters=filters
1021+
)
1022+
total_latest_filtered, latest_services_filtered = (
1023+
await services_repo.list_latest_services(
1024+
product_name=target_product, user_id=user_id, filters=filters
1025+
)
1026+
)
1027+
1028+
# Verify counts with key pattern filter
1029+
assert total_all_filtered == 3 # All 3 versions of service 1
1030+
assert total_latest_filtered == 1 # Only latest version of service 1
1031+
1032+
# Verify service key pattern is matched
1033+
assert all(s.key == service_key_1 for s in all_services_filtered)
1034+
assert all(s.key == service_key_1 for s in latest_services_filtered)
1035+
1036+
# Test 4: Pagination
1037+
# Get first page (limit=2)
1038+
total_all_page1, all_services_page1 = await services_repo.list_all_services(
1039+
product_name=target_product,
1040+
user_id=user_id,
1041+
pagination_limit=2,
1042+
pagination_offset=0,
1043+
)
1044+
1045+
# Get second page (limit=2, offset=2)
1046+
total_all_page2, all_services_page2 = await services_repo.list_all_services(
1047+
product_name=target_product,
1048+
user_id=user_id,
1049+
pagination_limit=2,
1050+
pagination_offset=2,
1051+
)
1052+
1053+
# Verify pagination
1054+
assert total_all_page1 == 6 # Total count should still be total
1055+
assert len(all_services_page1) == 2 # But only 2 items on first page
1056+
assert len(all_services_page2) == 2 # And 2 items on second page
1057+
1058+
# Ensure pages have different items
1059+
page1_key_versions = {(s.key, s.version) for s in all_services_page1}
1060+
page2_key_versions = {(s.key, s.version) for s in all_services_page2}
1061+
assert not page1_key_versions.intersection(page2_key_versions)
1062+
1063+
1064+
async def test_list_all_services_empty_database(
1065+
target_product: ProductName,
1066+
services_repo: ServicesRepository,
1067+
user_id: UserID,
1068+
):
1069+
"""Test list_all_services and list_latest_services with an empty database."""
1070+
# Test with empty database
1071+
total_all, all_services = await services_repo.list_all_services(
1072+
product_name=target_product, user_id=user_id
1073+
)
1074+
total_latest, latest_services = await services_repo.list_latest_services(
1075+
product_name=target_product, user_id=user_id
1076+
)
1077+
1078+
assert total_all == 0
1079+
assert len(all_services) == 0
1080+
assert total_latest == 0
1081+
assert len(latest_services) == 0
1082+
1083+
1084+
async def test_list_all_services_deprecated_versions(
1085+
target_product: ProductName,
1086+
create_fake_service_data: CreateFakeServiceDataCallable,
1087+
services_db_tables_injector: Callable,
1088+
services_repo: ServicesRepository,
1089+
user_id: UserID,
1090+
):
1091+
"""Test that list_all_services includes deprecated versions while list_latest_services ignores them."""
1092+
from datetime import datetime, timedelta
1093+
1094+
# Create a service with regular and deprecated versions
1095+
service_key = "simcore/services/dynamic/with-deprecated"
1096+
service_data = []
1097+
1098+
# Add regular version
1099+
service_data.append(
1100+
create_fake_service_data(
1101+
service_key,
1102+
"1.0.0",
1103+
team_access=None,
1104+
everyone_access=None,
1105+
product=target_product,
1106+
)
1107+
)
1108+
1109+
# Add deprecated version (with higher version number)
1110+
deprecated_service = create_fake_service_data(
1111+
service_key,
1112+
"2.0.0",
1113+
team_access=None,
1114+
everyone_access=None,
1115+
product=target_product,
1116+
)
1117+
# Set deprecated timestamp to yesterday
1118+
deprecated_service[0]["deprecated"] = datetime.now() - timedelta(days=1)
1119+
service_data.append(deprecated_service)
1120+
1121+
# Add newer non-deprecated version
1122+
service_data.append(
1123+
create_fake_service_data(
1124+
service_key,
1125+
"3.0.0",
1126+
team_access=None,
1127+
everyone_access=None,
1128+
product=target_product,
1129+
)
1130+
)
1131+
1132+
await services_db_tables_injector(service_data)
1133+
1134+
# Get all services - should include both deprecated and non-deprecated
1135+
total_all, all_services = await services_repo.list_all_services(
1136+
product_name=target_product, user_id=user_id
1137+
)
1138+
1139+
# Get latest services - should only show latest non-deprecated
1140+
total_latest, latest_services = await services_repo.list_latest_services(
1141+
product_name=target_product, user_id=user_id
1142+
)
1143+
1144+
# Verify counts
1145+
assert total_all == 3 # All 3 versions
1146+
1147+
# Verify latest is the newest non-deprecated version
1148+
assert len(latest_services) == 1
1149+
assert latest_services[0].key == service_key
1150+
assert latest_services[0].version == "3.0.0"
1151+
1152+
# Get versions from all services
1153+
versions = [s.version for s in all_services if s.key == service_key]
1154+
assert sorted(versions) == ["1.0.0", "2.0.0", "3.0.0"]
1155+
1156+
# Verify the deprecated status is correctly set
1157+
for service in all_services:
1158+
if service.key == service_key and service.version == "2.0.0":
1159+
assert service.deprecated is not None
1160+
else:
1161+
assert service.deprecated is None

0 commit comments

Comments
 (0)