Skip to content

Commit 9e0dd00

Browse files
committed
🏗️ Refactor service release compatibility checks and enhance metadata inheritance tests
1 parent a00abfa commit 9e0dd00

File tree

2 files changed

+163
-35
lines changed

2 files changed

+163
-35
lines changed

services/catalog/src/simcore_service_catalog/service/access_rights.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,38 +123,34 @@ async def evaluate_default_service_ownership_and_rights(
123123
return (owner_gid, default_access_rights)
124124

125125

126-
async def _find_previous_compatible_release(
126+
async def _find_latest_patch_compatible_release(
127127
services_repo: ServicesRepository, *, service_metadata: ServiceMetaDataPublished
128128
) -> ServiceMetaDataDBGet | None:
129129
"""
130-
Finds the previous compatible release for a service.
130+
Finds the previous patched release for a service.
131131
132132
Args:
133133
services_repo: Instance of ServicesRepository for database access.
134134
service_metadata: Metadata of the service being evaluated.
135135
136136
Returns:
137-
The previous compatible release if found, None otherwise.
137+
Latest patch release of the service if it exists, otherwise None.
138138
"""
139139
if _is_frontend_service(service_metadata):
140140
return None
141141

142142
new_version: Version = as_version(service_metadata.version)
143-
latest_releases = await services_repo.list_service_releases(
143+
patch_releases_latest_first = await services_repo.list_service_releases(
144144
service_metadata.key,
145145
major=new_version.major,
146146
minor=new_version.minor,
147-
limit_count=5,
148147
)
149148

150-
# FIXME: deprecated versions hsould not coutn!!!
151-
152149
# latest_releases is sorted from newer to older
153-
for release in latest_releases:
150+
for release in patch_releases_latest_first:
154151
# COMPATIBILITY RULE:
155152
# - a patch release is compatible with the previous patch release
156-
157-
# FIXME: not all compatible releases!!!
153+
# - WARNING: this does not account for custom compatibility policies!!!!
158154
if is_patch_release(new_version, release.version):
159155
return release
160156

@@ -169,7 +165,7 @@ async def inherit_from_latest_compatible_release(
169165
170166
This function applies inheritance policies:
171167
- AUTO-UPGRADE PATCH policy: new patch releases inherit access rights from previous compatible versions
172-
- Metadata inheritance: icon and other metadata fields are inherited if not specified in the new version
168+
- Metadata inheritance: icon and thumbnail fields are inherited if not specified in the new version
173169
174170
Args:
175171
services_repo: Instance of ServicesRepository for database access.
@@ -189,7 +185,7 @@ async def inherit_from_latest_compatible_release(
189185
"metadata_updates": {},
190186
}
191187

192-
previous_release = await _find_previous_compatible_release(
188+
previous_release = await _find_latest_patch_compatible_release(
193189
services_repo, service_metadata=service_metadata
194190
)
195191

@@ -210,10 +206,12 @@ async def inherit_from_latest_compatible_release(
210206
for access in previous_access_rights
211207
]
212208

213-
# 2. METADATA:
214-
# Inherit icon if not specified in the new service
209+
# 2. ServiceMetaDataPublished
210+
# Inherit some fields if not specified in the new service
215211
if not service_metadata.icon and previous_release.icon:
216212
inherited_data["metadata_updates"]["icon"] = previous_release.icon
213+
if not service_metadata.thumbnail and previous_release.thumbnail:
214+
inherited_data["metadata_updates"]["thumbnail"] = previous_release.thumbnail
217215

218216
return inherited_data
219217

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

Lines changed: 151 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# pylint: disable=unused-variable
44

55
from collections.abc import Callable
6-
from typing import Any
6+
from typing import Any, Protocol
77

88
import pytest
99
import simcore_service_catalog.service.access_rights
@@ -43,7 +43,9 @@ def new_service_metadata_published(user: dict[str, Any]) -> ServiceMetaDataPubli
4343
Author(name=user["name"], email=user["email"], affiliation=None)
4444
]
4545
metadata.version = TypeAdapter(ServiceVersion).validate_python("1.0.11")
46-
metadata.icon = None # Remove icon to test inheritance
46+
# Set all inheritable fields to None initially
47+
metadata.icon = None
48+
metadata.thumbnail = None
4749
return metadata
4850

4951

@@ -63,6 +65,47 @@ def app_with_repo(
6365
return app, services_repo
6466

6567

68+
class CreateLatetReleaseCallable(Protocol):
69+
"""Callable to create a latest release with specified metadata fields."""
70+
71+
async def __call__(self, metadata_fields: dict[str, Any]) -> dict[str, Any]: ...
72+
73+
74+
@pytest.fixture
75+
def create_latest_release(
76+
create_fake_service_data: CreateFakeServiceDataCallable,
77+
new_service_metadata_published: ServiceMetaDataPublished,
78+
target_product: ProductName,
79+
services_db_tables_injector: Callable,
80+
) -> CreateLatetReleaseCallable:
81+
"""Creates a latest release with specified metadata fields."""
82+
83+
from packaging.version import Version
84+
85+
new_version = Version(new_service_metadata_published.version)
86+
87+
async def _create(metadata_fields: dict[str, Any]) -> dict[str, Any]:
88+
latest_release_service, *latest_release_service_access_rights = (
89+
create_fake_service_data(
90+
new_service_metadata_published.key,
91+
f"{new_version.major}.{new_version.minor}.{new_version.micro-1}",
92+
team_access="x",
93+
everyone_access=None,
94+
product=target_product,
95+
)
96+
)
97+
98+
# Update with provided metadata fields
99+
for field, value in metadata_fields.items():
100+
latest_release_service[field] = value
101+
102+
latest_release = (latest_release_service, *latest_release_service_access_rights)
103+
await services_db_tables_injector([latest_release])
104+
return latest_release_service
105+
106+
return _create
107+
108+
66109
def test_reduce_access_rights():
67110
sample = ServiceAccessRightsDB.model_validate(
68111
{
@@ -118,14 +161,105 @@ def test_reduce_access_rights():
118161
}
119162

120163

164+
async def test_metadata_inheritance_variations(
165+
new_service_metadata_published: ServiceMetaDataPublished,
166+
app_with_repo: tuple[FastAPI, ServicesRepository],
167+
create_latest_release: CreateLatetReleaseCallable,
168+
):
169+
"""Test different variations of metadata inheritance with complete previous release."""
170+
app, services_repo = app_with_repo
171+
172+
# Set up a previous release with all metadata fields
173+
latest_release_service = await create_latest_release(
174+
{
175+
"icon": "https://foo/previous_icon.svg",
176+
"thumbnail": "https://foo/previous_thumbnail.jpg",
177+
}
178+
)
179+
180+
# Case 1: All fields missing in new service - only icon and thumbnail should be inherited
181+
new_service = new_service_metadata_published.model_copy(deep=True)
182+
inherited_data = await inherit_from_latest_compatible_release(
183+
services_repo, service_metadata=new_service
184+
)
185+
186+
assert inherited_data["metadata_updates"] == {
187+
"icon": latest_release_service["icon"],
188+
"thumbnail": latest_release_service["thumbnail"],
189+
}
190+
191+
# Case 2: Some fields present, others missing
192+
new_service = new_service_metadata_published.model_copy(
193+
deep=True,
194+
update={
195+
"thumbnail": None, # Missing field
196+
"icon": "https://foo/new_icon.svg",
197+
},
198+
)
199+
200+
inherited_data = await inherit_from_latest_compatible_release(
201+
services_repo, service_metadata=new_service
202+
)
203+
204+
# Only thumbnail should be inherited
205+
assert "icon" not in inherited_data["metadata_updates"]
206+
assert inherited_data["metadata_updates"] == {
207+
"thumbnail": latest_release_service["thumbnail"],
208+
}
209+
210+
# Case 3: All fields present in new service - nothing should be inherited
211+
new_service = new_service_metadata_published.model_copy(
212+
deep=True,
213+
update={
214+
"icon": "https://foo/new_icon.svg",
215+
"thumbnail": "https://foo/new_thumbnail.jpg",
216+
},
217+
)
218+
219+
inherited_data = await inherit_from_latest_compatible_release(
220+
services_repo, service_metadata=new_service
221+
)
222+
223+
# No metadata should be inherited
224+
assert inherited_data["metadata_updates"] == {}
225+
226+
227+
async def test_metadata_inheritance_with_incomplete_previous_release(
228+
new_service_metadata_published: ServiceMetaDataPublished,
229+
app_with_repo: tuple[FastAPI, ServicesRepository],
230+
create_latest_release: CreateLatetReleaseCallable,
231+
):
232+
"""Test metadata inheritance when previous release has incomplete metadata fields."""
233+
app, services_repo = app_with_repo
234+
235+
# Case 4: Previous release missing some fields
236+
latest_release_service = await create_latest_release(
237+
{
238+
"icon": "https://foo/previous_icon.svg",
239+
"thumbnail": "https://foo/previous_thumbnail.jpg",
240+
}
241+
)
242+
243+
new_service = new_service_metadata_published.model_copy(deep=True)
244+
inherited_data = await inherit_from_latest_compatible_release(
245+
services_repo, service_metadata=new_service
246+
)
247+
248+
# Only icon and thumbnail should be inherited
249+
assert inherited_data["metadata_updates"] == {
250+
"icon": latest_release_service["icon"],
251+
"thumbnail": latest_release_service["thumbnail"],
252+
}
253+
assert "description" not in inherited_data["metadata_updates"]
254+
255+
121256
async def test_service_upgrade_metadata_inheritance_old_service(
122257
user_groups_ids: list[GroupID],
123258
target_product: ProductName,
124-
services_db_tables_injector: Callable,
125-
create_fake_service_data: CreateFakeServiceDataCallable,
126259
mocker: MockerFixture,
127260
new_service_metadata_published: ServiceMetaDataPublished,
128261
app_with_repo: tuple[FastAPI, ServicesRepository],
262+
create_latest_release: CreateLatetReleaseCallable,
129263
):
130264
"""Test inheritance behavior when the service is considered old"""
131265
everyone_gid, user_gid, team_gid = user_groups_ids
@@ -138,22 +272,15 @@ async def test_service_upgrade_metadata_inheritance_old_service(
138272
return_value=True,
139273
)
140274

141-
# Create latest-release service for testing inheritance
142-
latest_release_service, *latest_release_service_access_rights = (
143-
create_fake_service_data(
144-
new_service_metadata_published.key,
145-
"1.0.10",
146-
team_access="x",
147-
everyone_access=None,
148-
product=target_product,
149-
)
275+
# Create latest-release service for testing inheritance with all metadata fields
276+
latest_release_service = await create_latest_release(
277+
{
278+
"icon": "https://foo/previous_icon.svg",
279+
"description": "Previous description", # This won't be inherited
280+
"thumbnail": "https://foo/previous_thumbnail.jpg",
281+
}
150282
)
151283

152-
latest_release_service["icon"] = "https://foo/previous_icon.svg"
153-
latest_release = (latest_release_service, *latest_release_service_access_rights)
154-
155-
await services_db_tables_injector([latest_release])
156-
157284
# DEFAULT policies for old service
158285
owner_gid, service_access_rights = (
159286
await evaluate_default_service_ownership_and_rights(
@@ -185,10 +312,13 @@ async def test_service_upgrade_metadata_inheritance_old_service(
185312
services_repo, service_metadata=new_service_metadata_published
186313
)
187314

188-
# Check metadata inheritance
315+
# Check metadata inheritance - only icon and thumbnail should be inherited
189316
inherited_metadata = inherited_data["metadata_updates"]
190-
assert "icon" in inherited_metadata
191-
assert inherited_metadata["icon"] == latest_release_service["icon"]
317+
assert "description" not in inherited_metadata
318+
assert inherited_metadata == {
319+
"icon": latest_release_service["icon"],
320+
"thumbnail": latest_release_service["thumbnail"],
321+
}
192322

193323

194324
async def test_service_upgrade_metadata_inheritance_new_service_multi_product(

0 commit comments

Comments
 (0)