Merge ~ruinedyourlife/launchpad:feat-archive-metadata-overrides into launchpad:master
- Git
- lp:~ruinedyourlife/launchpad
- feat-archive-metadata-overrides
- Merge into master
Status: | Merged |
---|---|
Approved by: | Quentin Debhi |
Approved revision: | b8d89e18bdb39d34963ee0d1817ae237a4c3a07c |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~ruinedyourlife/launchpad:feat-archive-metadata-overrides |
Merge into: | launchpad:master |
Diff against target: |
819 lines (+648/-1) 6 files modified
lib/lp/soyuz/browser/tests/test_archive_webservice.py (+308/-0) lib/lp/soyuz/interfaces/archive.py (+35/-0) lib/lp/soyuz/model/archive.py (+35/-0) lib/lp/soyuz/stories/webservice/xx-archive.rst (+1/-0) lib/lp/soyuz/tests/test_archive.py (+267/-1) lib/lp/testing/factory.py (+2/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Guruprasad | Approve | ||
Review via email: mp+463030@code.launchpad.net |
Commit message
Add new field Archive.
This field can be used for storing per-archive metadata overrides.
Also expose this field via the API and allow authorized users to
view and set its values.
Description of the change
Ines Almeida (ines-almeida) wrote : | # |
Quentin Debhi (ruinedyourlife) wrote : | # |
I glanced over the tests, and I didn't see anything that called for an update, besides the factory method itself to populate the `metadata_
Adding though, I wouldn't know at all.
Thanks for the insight!
Guruprasad (lgp171188) wrote : | # |
This is a great start. I have left some review comments to address below.
Guruprasad (lgp171188) : | # |
Guruprasad (lgp171188) wrote : | # |
Like we discussed in the code review face-to-face meeting, a non-subscriber, non-owner, non-admin user should not get any information about the existence of a private archive, but it looks like that is happening in a test.
Can you create an empty archive in production, request Clint to make it private and share the URL with me so that we can test and verify the expected behaviour?
The changes look good to me and I will take a quick look and approve after you have addressed all the comments.
Guruprasad (lgp171188) wrote (last edit ): | # |
> Like we discussed in the code review face-to-face meeting, a non-subscriber,
> non-owner, non-admin user should not get any information about the existence
> of a private archive, but it looks like that is happening in a test.
>
> Can you create an empty archive in production, request Clint to make it
> private and share the URL with me so that we can test and verify the expected
> behaviour?
We tested this in production by creating a PPA and trying to access it via the API as an anonymous user and a logged-in non-subscriber user and I can confirm the API does return non-private details about the archive. Relevant Mattermost chat conversation logs - https:/
Quentin Debhi (ruinedyourlife) : | # |
Quentin Debhi (ruinedyourlife) : | # |
Guruprasad (lgp171188) wrote : | # |
LGTM 👍 Please address the open review comments before squashing the commits into one, force pushing to this MP's source branch, and then set the status of this MP to 'Approved'.
Quentin Debhi (ruinedyourlife) : | # |
Preview Diff
1 | diff --git a/lib/lp/soyuz/browser/tests/test_archive_webservice.py b/lib/lp/soyuz/browser/tests/test_archive_webservice.py |
2 | index 598a015..57130d6 100644 |
3 | --- a/lib/lp/soyuz/browser/tests/test_archive_webservice.py |
4 | +++ b/lib/lp/soyuz/browser/tests/test_archive_webservice.py |
5 | @@ -35,6 +35,7 @@ from lp.testing import ( |
6 | TestCaseWithFactory, |
7 | admin_logged_in, |
8 | api_url, |
9 | + celebrity_logged_in, |
10 | login, |
11 | person_logged_in, |
12 | record_two_runs, |
13 | @@ -1266,3 +1267,310 @@ class TestArchiveSet(TestCaseWithFactory): |
14 | .jsonBody() |
15 | ) |
16 | self.assertEqual(body["reference"], reference) |
17 | + |
18 | + |
19 | +class TestArchiveMetadataOverridesWebService(TestCaseWithFactory): |
20 | + layer = DatabaseFunctionalLayer |
21 | + |
22 | + def setUp(self): |
23 | + super().setUp() |
24 | + self.default_overrides = { |
25 | + "Origin": "default_origin", |
26 | + "Label": "default_label", |
27 | + "Suite": "default_suite", |
28 | + "Snapshots": "default_snapshots", |
29 | + } |
30 | + |
31 | + def create_archive(self, owner=None, private=False, primary=False): |
32 | + if primary: |
33 | + distribution = self.factory.makeDistribution(owner=owner) |
34 | + archive = self.factory.makeArchive( |
35 | + owner=owner, |
36 | + distribution=distribution, |
37 | + purpose=ArchivePurpose.PRIMARY, |
38 | + ) |
39 | + with celebrity_logged_in("admin"): |
40 | + archive.setMetadataOverrides(self.default_overrides) |
41 | + return archive |
42 | + |
43 | + return self.factory.makeArchive( |
44 | + owner=owner, |
45 | + private=private, |
46 | + metadata_overrides=self.default_overrides, |
47 | + ) |
48 | + |
49 | + def get_and_check_response(self, person, archive, expected_body=None): |
50 | + archive_url = api_url(archive) |
51 | + webservice = webservice_for_person( |
52 | + person, |
53 | + permission=OAuthPermission.WRITE_PRIVATE, |
54 | + default_api_version="devel", |
55 | + ) |
56 | + response = webservice.get(archive_url) |
57 | + self.assertEqual(200, response.status) |
58 | + if expected_body: |
59 | + self.assertEqual( |
60 | + expected_body, |
61 | + response.jsonBody()["metadata_overrides"], |
62 | + ) |
63 | + |
64 | + def patch_and_check_response( |
65 | + self, person, archive, overrides, expected_status, expected_body=None |
66 | + ): |
67 | + archive_url = api_url(archive) |
68 | + webservice = webservice_for_person( |
69 | + person, |
70 | + permission=OAuthPermission.WRITE_PRIVATE, |
71 | + default_api_version="devel", |
72 | + ) |
73 | + response = webservice.patch( |
74 | + archive_url, |
75 | + "application/json", |
76 | + json.dumps({"metadata_overrides": overrides}), |
77 | + ) |
78 | + self.assertEqual(expected_status, response.status) |
79 | + if expected_body: |
80 | + with person_logged_in(person): |
81 | + self.assertEqual(expected_body, archive.metadata_overrides) |
82 | + |
83 | + def patch_and_check_structure( |
84 | + self, webservice, achive_url, data, expected_status, expected_body |
85 | + ): |
86 | + response = webservice.patch( |
87 | + achive_url, "application/json", json.dumps(data) |
88 | + ) |
89 | + self.assertThat( |
90 | + response, |
91 | + MatchesStructure.byEquality( |
92 | + status=expected_status, |
93 | + body=expected_body, |
94 | + ), |
95 | + ) |
96 | + |
97 | + def test_cannot_set_invalid_metadata_keys(self): |
98 | + owner = self.factory.makePerson() |
99 | + archive = self.create_archive(owner=owner) |
100 | + archive_url = api_url(archive) |
101 | + webservice = webservice_for_person( |
102 | + owner, |
103 | + permission=OAuthPermission.WRITE_PRIVATE, |
104 | + default_api_version="devel", |
105 | + ) |
106 | + invalid_overrides = {"metadata_overrides": {"Invalid": "test_invalid"}} |
107 | + self.patch_and_check_structure( |
108 | + webservice, |
109 | + archive_url, |
110 | + invalid_overrides, |
111 | + 400, |
112 | + ( |
113 | + b"Invalid metadata override key. Allowed keys are " |
114 | + b"{'Label', 'Origin', 'Snapshots', 'Suite'}." |
115 | + ), |
116 | + ) |
117 | + |
118 | + def test_cannot_set_non_string_values_for_metadata(self): |
119 | + owner = self.factory.makePerson() |
120 | + archive = self.create_archive(owner=owner) |
121 | + archive_url = api_url(archive) |
122 | + webservice = webservice_for_person( |
123 | + owner, |
124 | + permission=OAuthPermission.WRITE_PRIVATE, |
125 | + default_api_version="devel", |
126 | + ) |
127 | + invalid_values = ["", None, True, 1, [], {}] |
128 | + for value in invalid_values: |
129 | + data = {"metadata_overrides": {"Origin": value}} |
130 | + self.patch_and_check_structure( |
131 | + webservice, |
132 | + archive_url, |
133 | + data, |
134 | + 400, |
135 | + b"Value for 'Origin' must be a non-empty string.", |
136 | + ) |
137 | + |
138 | + def test_non_owner_can_view_public_archive_metadata_overrides(self): |
139 | + user = self.factory.makePerson() |
140 | + archive = self.create_archive() |
141 | + self.get_and_check_response(user, archive, self.default_overrides) |
142 | + |
143 | + def test_owner_can_view_own_public_archive_metadata_overrides(self): |
144 | + owner = self.factory.makePerson() |
145 | + archive = self.create_archive(owner=owner) |
146 | + self.get_and_check_response(owner, archive, self.default_overrides) |
147 | + |
148 | + def test_admin_can_view_metadata_overrides_of_any_public_archive(self): |
149 | + admin = self.factory.makeAdministrator() |
150 | + archive = self.create_archive() |
151 | + self.get_and_check_response(admin, archive, self.default_overrides) |
152 | + |
153 | + def test_owner_can_view_own_private_archive_metadata_overrides(self): |
154 | + owner = self.factory.makePerson() |
155 | + private_archive = self.create_archive(owner=owner, private=True) |
156 | + self.get_and_check_response( |
157 | + owner, private_archive, self.default_overrides |
158 | + ) |
159 | + |
160 | + def test_admin_can_view_metadata_overrides_of_any_private_archive(self): |
161 | + admin = self.factory.makeAdministrator() |
162 | + private_archive = self.create_archive(private=True) |
163 | + self.get_and_check_response( |
164 | + admin, private_archive, self.default_overrides |
165 | + ) |
166 | + |
167 | + def test_non_owner_cannot_view_private_archive_metadata_overrides(self): |
168 | + owner = self.factory.makePerson() |
169 | + user = self.factory.makePerson() |
170 | + private_archive = self.create_archive(owner=owner, private=True) |
171 | + self.get_and_check_response( |
172 | + user, |
173 | + private_archive, |
174 | + "tag:launchpad.net:2008:redacted", |
175 | + ) |
176 | + |
177 | + def test_subscriber_cannot_view_private_archive_metadata_overrides(self): |
178 | + owner = self.factory.makePerson() |
179 | + user = self.factory.makePerson() |
180 | + private_archive = self.create_archive(owner=owner, private=True) |
181 | + with person_logged_in(owner): |
182 | + private_archive.newSubscription(user, owner) |
183 | + self.get_and_check_response( |
184 | + user, |
185 | + private_archive, |
186 | + "tag:launchpad.net:2008:redacted", |
187 | + ) |
188 | + |
189 | + def test_owner_can_set_metadata_overrides_on_own_public_archive(self): |
190 | + owner = self.factory.makePerson() |
191 | + archive = self.create_archive(owner=owner) |
192 | + with person_logged_in(owner): |
193 | + self.assertEqual( |
194 | + archive.metadata_overrides, self.default_overrides |
195 | + ) |
196 | + overrides = {"Origin": "test_origin"} |
197 | + self.patch_and_check_response( |
198 | + owner, archive, overrides, 209, overrides |
199 | + ) |
200 | + |
201 | + def test_admin_can_set_metadata_overrides_on_any_public_archive(self): |
202 | + admin = self.factory.makeAdministrator() |
203 | + archive = self.create_archive() |
204 | + with celebrity_logged_in("admin"): |
205 | + self.assertEqual( |
206 | + archive.metadata_overrides, self.default_overrides |
207 | + ) |
208 | + overrides = {"Origin": "test_origin"} |
209 | + self.patch_and_check_response( |
210 | + admin, archive, overrides, 209, overrides |
211 | + ) |
212 | + |
213 | + def test_non_owner_cannot_set_metadata_overrides_on_public_archive(self): |
214 | + owner = self.factory.makePerson() |
215 | + user = self.factory.makePerson() |
216 | + archive = self.create_archive(owner=owner) |
217 | + archive_url = api_url(archive) |
218 | + webservice = webservice_for_person( |
219 | + user, |
220 | + permission=OAuthPermission.WRITE_PRIVATE, |
221 | + default_api_version="devel", |
222 | + ) |
223 | + overrides = {"metadata_overrides": {"Origin": "test_origin"}} |
224 | + self.patch_and_check_structure( |
225 | + webservice, |
226 | + archive_url, |
227 | + overrides, |
228 | + 401, |
229 | + b"(<Archive object>, 'setMetadataOverrides', 'launchpad.Edit')", |
230 | + ) |
231 | + |
232 | + def test_owner_can_set_metadata_overrides_on_private_archive(self): |
233 | + owner = self.factory.makePerson() |
234 | + private_archive = self.create_archive(owner=owner, private=True) |
235 | + with person_logged_in(owner): |
236 | + self.assertEqual( |
237 | + private_archive.metadata_overrides, self.default_overrides |
238 | + ) |
239 | + overrides = {"Origin": "test_origin"} |
240 | + self.patch_and_check_response( |
241 | + owner, private_archive, overrides, 209, overrides |
242 | + ) |
243 | + |
244 | + def test_admin_can_set_metadata_overrides_on_any_private_archive(self): |
245 | + admin = self.factory.makeAdministrator() |
246 | + private_archive = self.create_archive(private=True) |
247 | + with celebrity_logged_in("admin"): |
248 | + self.assertEqual( |
249 | + private_archive.metadata_overrides, self.default_overrides |
250 | + ) |
251 | + overrides = {"Origin": "test_origin"} |
252 | + self.patch_and_check_response( |
253 | + admin, private_archive, overrides, 209, overrides |
254 | + ) |
255 | + |
256 | + def test_non_owner_cannot_set_metadata_overrides_on_private_archive(self): |
257 | + user = self.factory.makePerson() |
258 | + private_archive = self.create_archive(primary=True) |
259 | + archive_url = api_url(private_archive) |
260 | + webservice = webservice_for_person( |
261 | + user, |
262 | + permission=OAuthPermission.WRITE_PRIVATE, |
263 | + default_api_version="devel", |
264 | + ) |
265 | + overrides = {"metadata_overrides": {"Origin": "test_origin"}} |
266 | + self.patch_and_check_structure( |
267 | + webservice, |
268 | + archive_url, |
269 | + overrides, |
270 | + 401, |
271 | + b"(<Archive object>, 'setMetadataOverrides', 'launchpad.Edit')", |
272 | + ) |
273 | + |
274 | + def test_subscriber_cannot_set_metadata_overrides_on_private_archive(self): |
275 | + owner = self.factory.makePerson() |
276 | + user = self.factory.makePerson() |
277 | + private_archive = self.create_archive(owner=owner, private=True) |
278 | + with person_logged_in(owner): |
279 | + private_archive.newSubscription(user, owner) |
280 | + overrides = {"Origin": "test_origin"} |
281 | + self.patch_and_check_response(user, private_archive, overrides, 401) |
282 | + |
283 | + def test_owner_can_set_metadata_overrides_on_own_primary_archive(self): |
284 | + owner = self.factory.makePerson() |
285 | + primary_archive = self.create_archive(owner=owner, primary=True) |
286 | + with person_logged_in(owner): |
287 | + self.assertEqual( |
288 | + primary_archive.metadata_overrides, self.default_overrides |
289 | + ) |
290 | + overrides = {"Origin": "test_origin"} |
291 | + self.patch_and_check_response( |
292 | + owner, primary_archive, overrides, 209, overrides |
293 | + ) |
294 | + |
295 | + def test_admin_can_set_metadata_overrides_on_any_primary_archive(self): |
296 | + admin = self.factory.makeAdministrator() |
297 | + primary_archive = self.create_archive(primary=True) |
298 | + with celebrity_logged_in("admin"): |
299 | + self.assertEqual( |
300 | + primary_archive.metadata_overrides, self.default_overrides |
301 | + ) |
302 | + overrides = {"Origin": "test_origin"} |
303 | + self.patch_and_check_response( |
304 | + admin, primary_archive, overrides, 209, overrides |
305 | + ) |
306 | + |
307 | + def test_non_owner_cannot_set_metadata_overrides_on_primary_archive(self): |
308 | + user = self.factory.makePerson() |
309 | + primary_archive = self.create_archive(primary=True) |
310 | + archive_url = api_url(primary_archive) |
311 | + webservice = webservice_for_person( |
312 | + user, |
313 | + permission=OAuthPermission.WRITE_PRIVATE, |
314 | + default_api_version="devel", |
315 | + ) |
316 | + overrides = {"metadata_overrides": {"Origin": "test_origin"}} |
317 | + self.patch_and_check_structure( |
318 | + webservice, |
319 | + archive_url, |
320 | + overrides, |
321 | + 401, |
322 | + b"(<Archive object>, 'setMetadataOverrides', 'launchpad.Edit')", |
323 | + ) |
324 | diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py |
325 | index 780895a..f580599 100644 |
326 | --- a/lib/lp/soyuz/interfaces/archive.py |
327 | +++ b/lib/lp/soyuz/interfaces/archive.py |
328 | @@ -72,6 +72,7 @@ from lazr.restful.declarations import ( |
329 | exported, |
330 | exported_as_webservice_collection, |
331 | exported_as_webservice_entry, |
332 | + mutator_for, |
333 | operation_for_version, |
334 | operation_parameters, |
335 | operation_returns_collection_of, |
336 | @@ -79,11 +80,13 @@ from lazr.restful.declarations import ( |
337 | rename_parameters_as, |
338 | ) |
339 | from lazr.restful.fields import CollectionField, Reference |
340 | +from lazr.restful.interface import copy_field |
341 | from zope.interface import Attribute, Interface |
342 | from zope.schema import ( |
343 | Bool, |
344 | Choice, |
345 | Datetime, |
346 | + Dict, |
347 | Int, |
348 | List, |
349 | Object, |
350 | @@ -1021,6 +1024,24 @@ class IArchiveView(IHasBuildRecords): |
351 | "pending publications." |
352 | ) |
353 | |
354 | + metadata_overrides = exported( |
355 | + Dict( |
356 | + title=_( |
357 | + "A JSON object containing metadata overrides for this archive." |
358 | + ), |
359 | + description=_( |
360 | + "Accepted keys are ... The values for all these keys should " |
361 | + "be a string and can use a '{series}' placeholder that will " |
362 | + "get substituted the name of the series that is currently " |
363 | + "being published." |
364 | + ), |
365 | + key_type=TextLine(), |
366 | + required=False, |
367 | + readonly=True, |
368 | + ), |
369 | + as_of="devel", |
370 | + ) |
371 | + |
372 | processors = exported( |
373 | CollectionField( |
374 | title=_("Processors"), |
375 | @@ -2664,6 +2685,19 @@ class IArchiveEdit(Interface): |
376 | :param names: A list of token names. |
377 | """ |
378 | |
379 | + @mutator_for(IArchiveView["metadata_overrides"]) |
380 | + @operation_parameters( |
381 | + metadata_overrides=copy_field(IArchiveView["metadata_overrides"]), |
382 | + ) |
383 | + @export_write_operation() |
384 | + @operation_for_version("devel") |
385 | + def setMetadataOverrides(metadata_overrides): |
386 | + """Set the metadata overrides for this archive. |
387 | + |
388 | + :param metadata_overrides: A JSON object containing metadata overrides |
389 | + for this archive. |
390 | + """ |
391 | + |
392 | |
393 | class IArchiveDelete(Interface): |
394 | """Archive interface for operations restricted by delete privilege.""" |
395 | @@ -2772,6 +2806,7 @@ class IArchiveSet(Interface): |
396 | processors=None, |
397 | publishing_method=None, |
398 | repository_format=None, |
399 | + metadata_overrides=None, |
400 | ): |
401 | """Create a new archive. |
402 | |
403 | diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py |
404 | index f57a931..3e23a51 100644 |
405 | --- a/lib/lp/soyuz/model/archive.py |
406 | +++ b/lib/lp/soyuz/model/archive.py |
407 | @@ -11,6 +11,7 @@ __all__ = [ |
408 | "validate_ppa", |
409 | ] |
410 | |
411 | +import http.client |
412 | import logging |
413 | import re |
414 | import typing |
415 | @@ -20,6 +21,8 @@ from pathlib import PurePath |
416 | |
417 | import six |
418 | from lazr.lifecycle.event import ObjectCreatedEvent |
419 | +from lazr.restful.declarations import error_status |
420 | +from storm.databases.postgres import JSON as PgJSON |
421 | from storm.expr import ( |
422 | Alias, |
423 | And, |
424 | @@ -196,6 +199,13 @@ ARCHIVE_REFERENCE_TEMPLATES = { |
425 | } |
426 | |
427 | |
428 | +@error_status(http.client.BAD_REQUEST) |
429 | +class CannotSetMetadataOverrides(Exception): |
430 | + """Raised when someone tries to set invalid metadata overrides keys or |
431 | + non empty string values. |
432 | + """ |
433 | + |
434 | + |
435 | def storm_validate_external_dependencies(archive, attr, value): |
436 | assert attr == "external_dependencies" |
437 | errors = validate_external_dependencies(value) |
438 | @@ -366,6 +376,8 @@ class Archive(StormBase): |
439 | |
440 | dirty_suites = JSON(name="dirty_suites", allow_none=True) |
441 | |
442 | + metadata_overrides = PgJSON(name="metadata_overrides", allow_none=True) |
443 | + |
444 | _publishing_method = DBEnum( |
445 | name="publishing_method", allow_none=True, enum=ArchivePublishingMethod |
446 | ) |
447 | @@ -390,6 +402,7 @@ class Archive(StormBase): |
448 | signing_key_fingerprint=None, |
449 | publishing_method=None, |
450 | repository_format=None, |
451 | + metadata_overrides=None, |
452 | ): |
453 | super().__init__() |
454 | try: |
455 | @@ -406,6 +419,7 @@ class Archive(StormBase): |
456 | self.signing_key_fingerprint = signing_key_fingerprint |
457 | self.publishing_method = publishing_method |
458 | self.repository_format = repository_format |
459 | + self.metadata_overrides = metadata_overrides |
460 | except Exception: |
461 | # If validating references such as `owner` fails, then the new |
462 | # object may have been added to the store first. Remove it |
463 | @@ -2733,6 +2747,25 @@ class Archive(StormBase): |
464 | token_set = getUtility(IArchiveAuthTokenSet) |
465 | token_set.deactivateNamedTokensForArchive(self, names) |
466 | |
467 | + def setMetadataOverrides(self, metadata_overrides): |
468 | + """See `IArchive`.""" |
469 | + allowed_keys = sorted(["Origin", "Label", "Suite", "Snapshots"]) |
470 | + |
471 | + if not all(key in allowed_keys for key in metadata_overrides.keys()): |
472 | + allowed_keys_str = ", ".join(f"'{k}'" for k in allowed_keys) |
473 | + raise CannotSetMetadataOverrides( |
474 | + "Invalid metadata override key. Allowed keys are " |
475 | + f"{{{allowed_keys_str}}}." |
476 | + ) |
477 | + |
478 | + for key, value in metadata_overrides.items(): |
479 | + if not isinstance(value, str) or not value: |
480 | + raise CannotSetMetadataOverrides( |
481 | + f"Value for '{key}' must be a non-empty string." |
482 | + ) |
483 | + |
484 | + self.metadata_overrides = metadata_overrides |
485 | + |
486 | def newSubscription( |
487 | self, subscriber, registrant, date_expires=None, description=None |
488 | ): |
489 | @@ -3430,6 +3463,7 @@ class ArchiveSet: |
490 | processors=None, |
491 | publishing_method=ArchivePublishingMethod.LOCAL, |
492 | repository_format=ArchiveRepositoryFormat.DEBIAN, |
493 | + metadata_overrides=None, |
494 | ): |
495 | """See `IArchiveSet`.""" |
496 | if distribution is None: |
497 | @@ -3516,6 +3550,7 @@ class ArchiveSet: |
498 | require_virtualized=require_virtualized, |
499 | publishing_method=publishing_method, |
500 | repository_format=repository_format, |
501 | + metadata_overrides=metadata_overrides, |
502 | ) |
503 | |
504 | # Upon creation archives are enabled by default. |
505 | diff --git a/lib/lp/soyuz/stories/webservice/xx-archive.rst b/lib/lp/soyuz/stories/webservice/xx-archive.rst |
506 | index b308252..931c4d2 100644 |
507 | --- a/lib/lp/soyuz/stories/webservice/xx-archive.rst |
508 | +++ b/lib/lp/soyuz/stories/webservice/xx-archive.rst |
509 | @@ -56,6 +56,7 @@ For "devel" additional attributes are available. |
510 | enabled_restricted_processors_collection_link: |
511 | 'http://.../~cprov/+archive/ubuntu/ppa/enabled_restricted_processors' |
512 | external_dependencies: None |
513 | + metadata_overrides: None |
514 | name: 'ppa' |
515 | owner_link: 'http://.../~cprov' |
516 | permit_obsolete_series_uploads: False |
517 | diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py |
518 | index 42a8fba..01e0708 100644 |
519 | --- a/lib/lp/soyuz/tests/test_archive.py |
520 | +++ b/lib/lp/soyuz/tests/test_archive.py |
521 | @@ -113,7 +113,11 @@ from lp.soyuz.interfaces.publishing import ( |
522 | active_publishing_status, |
523 | inactive_publishing_status, |
524 | ) |
525 | -from lp.soyuz.model.archive import Archive, validate_ppa |
526 | +from lp.soyuz.model.archive import ( |
527 | + Archive, |
528 | + CannotSetMetadataOverrides, |
529 | + validate_ppa, |
530 | +) |
531 | from lp.soyuz.model.archivepermission import ( |
532 | ArchivePermission, |
533 | ArchivePermissionSet, |
534 | @@ -6975,3 +6979,265 @@ class TestArchivePermissions(TestCaseWithFactory): |
535 | archive = self.factory.makeArchive(owner=owner) |
536 | login_person(archive.owner) |
537 | self.assertTrue(check_permission("launchpad.Admin", archive)) |
538 | + |
539 | + |
540 | +class TestArchiveMetadataOverrides(TestCaseWithFactory): |
541 | + layer = DatabaseFunctionalLayer |
542 | + |
543 | + def setUp(self): |
544 | + super().setUp() |
545 | + self.default_metadata_overrides = { |
546 | + "Origin": "default_origin", |
547 | + "Label": "default_label", |
548 | + "Suite": "default_suite", |
549 | + "Snapshots": "default_snapshots", |
550 | + } |
551 | + |
552 | + def create_archive(self, owner=None, private=False, primary=False): |
553 | + if primary: |
554 | + distribution = self.factory.makeDistribution(owner=owner) |
555 | + archive = self.factory.makeArchive( |
556 | + owner=owner, |
557 | + distribution=distribution, |
558 | + purpose=ArchivePurpose.PRIMARY, |
559 | + ) |
560 | + with celebrity_logged_in("admin"): |
561 | + archive.setMetadataOverrides(self.default_metadata_overrides) |
562 | + return archive |
563 | + |
564 | + return self.factory.makeArchive( |
565 | + owner=owner, |
566 | + private=private, |
567 | + metadata_overrides=self.default_metadata_overrides, |
568 | + ) |
569 | + |
570 | + def test_cannot_set_invalid_metadata_keys(self): |
571 | + owner = self.factory.makePerson() |
572 | + archive = self.create_archive(owner=owner) |
573 | + login_person(owner) |
574 | + self.assertRaises( |
575 | + CannotSetMetadataOverrides, |
576 | + archive.setMetadataOverrides, |
577 | + {"Invalid": "test_invalid"}, |
578 | + ) |
579 | + |
580 | + def test_cannot_set_non_string_values_for_metadata(self): |
581 | + owner = self.factory.makePerson() |
582 | + archive = self.create_archive(owner=owner) |
583 | + login_person(owner) |
584 | + invalid_values = ["", None, True, 1, [], {}] |
585 | + for value in invalid_values: |
586 | + self.assertRaises( |
587 | + CannotSetMetadataOverrides, |
588 | + archive.setMetadataOverrides, |
589 | + {"Origin": value}, |
590 | + ) |
591 | + |
592 | + def test_anonymous_can_view_public_archive_metadata_overrides(self): |
593 | + archive = self.create_archive() |
594 | + login(ANONYMOUS) |
595 | + self.assertEqual( |
596 | + archive.metadata_overrides, |
597 | + self.default_metadata_overrides, |
598 | + ) |
599 | + |
600 | + def test_non_owner_can_view_public_archive_metadata_overrides(self): |
601 | + user = self.factory.makePerson() |
602 | + archive = self.create_archive() |
603 | + login_person(user) |
604 | + self.assertEqual( |
605 | + archive.metadata_overrides, |
606 | + self.default_metadata_overrides, |
607 | + ) |
608 | + |
609 | + def test_owner_can_view_own_public_archive_metadata_overrides(self): |
610 | + owner = self.factory.makePerson() |
611 | + archive = self.create_archive(owner=owner) |
612 | + login_person(owner) |
613 | + self.assertEqual( |
614 | + archive.metadata_overrides, |
615 | + self.default_metadata_overrides, |
616 | + ) |
617 | + |
618 | + def test_admin_can_view_metadata_overrides_of_any_public_archive(self): |
619 | + archive = self.create_archive() |
620 | + login_celebrity("admin") |
621 | + self.assertEqual( |
622 | + archive.metadata_overrides, |
623 | + self.default_metadata_overrides, |
624 | + ) |
625 | + |
626 | + def test_owner_can_view_own_private_archive_metadata_overrides(self): |
627 | + owner = self.factory.makePerson() |
628 | + private_archive = self.create_archive(owner=owner, private=True) |
629 | + login_person(owner) |
630 | + self.assertEqual( |
631 | + private_archive.metadata_overrides, |
632 | + self.default_metadata_overrides, |
633 | + ) |
634 | + |
635 | + def test_admin_can_view_metadata_overrides_of_any_private_archive(self): |
636 | + private_archive = self.create_archive(private=True) |
637 | + login_celebrity("admin") |
638 | + self.assertEqual( |
639 | + private_archive.metadata_overrides, |
640 | + self.default_metadata_overrides, |
641 | + ) |
642 | + |
643 | + def test_anonymous_cannot_view_private_archive_metadata_overrides(self): |
644 | + private_archive = self.create_archive(private=True) |
645 | + login(ANONYMOUS) |
646 | + self.assertRaises( |
647 | + Unauthorized, getattr, private_archive, "metadata_overrides" |
648 | + ) |
649 | + |
650 | + def test_non_owner_cannot_view_private_archive_metadata_overrides(self): |
651 | + owner = self.factory.makePerson() |
652 | + user = self.factory.makePerson() |
653 | + private_archive = self.create_archive(owner=owner, private=True) |
654 | + login_person(user) |
655 | + self.assertRaises( |
656 | + Unauthorized, getattr, private_archive, "metadata_overrides" |
657 | + ) |
658 | + |
659 | + def test_subscriber_cannot_view_private_archive_metadata_overrides(self): |
660 | + owner = self.factory.makePerson() |
661 | + user = self.factory.makePerson() |
662 | + private_archive = self.create_archive(owner=owner, private=True) |
663 | + with person_logged_in(owner): |
664 | + private_archive.newSubscription(user, owner) |
665 | + login_person(user) |
666 | + self.assertRaises( |
667 | + Unauthorized, getattr, private_archive, "metadata_overrides" |
668 | + ) |
669 | + |
670 | + def test_owner_can_set_metadata_overrides_on_own_public_archive(self): |
671 | + owner = self.factory.makePerson() |
672 | + login_person(owner) |
673 | + archive = self.create_archive(owner=owner) |
674 | + self.assertEqual( |
675 | + archive.metadata_overrides, self.default_metadata_overrides |
676 | + ) |
677 | + overrides = {"Origin": "test_origin"} |
678 | + archive.setMetadataOverrides(overrides) |
679 | + self.assertEqual(archive.metadata_overrides, overrides) |
680 | + |
681 | + def test_admin_can_set_metadata_overrides_on_any_public_archive(self): |
682 | + archive = self.create_archive() |
683 | + login_celebrity("admin") |
684 | + self.assertEqual( |
685 | + archive.metadata_overrides, self.default_metadata_overrides |
686 | + ) |
687 | + overrides = {"Origin": "test_origin"} |
688 | + archive.setMetadataOverrides(overrides) |
689 | + self.assertEqual(archive.metadata_overrides, overrides) |
690 | + |
691 | + def test_anonymous_cannot_set_metadata_overrides_on_public_archive(self): |
692 | + archive = self.create_archive() |
693 | + login(ANONYMOUS) |
694 | + overrides = {"Origin": "test_origin"} |
695 | + self.assertRaises( |
696 | + Unauthorized, lambda: archive.setMetadataOverrides(overrides) |
697 | + ) |
698 | + |
699 | + def test_non_owner_cannot_set_metadata_overrides_on_public_archive(self): |
700 | + user = self.factory.makePerson() |
701 | + archive = self.create_archive() |
702 | + login_person(user) |
703 | + overrides = {"Origin": "test_origin"} |
704 | + self.assertRaises( |
705 | + Unauthorized, lambda: archive.setMetadataOverrides(overrides) |
706 | + ) |
707 | + |
708 | + def test_owner_can_set_metadata_overrides_on_private_archive(self): |
709 | + owner = self.factory.makePerson() |
710 | + login_person(owner) |
711 | + private_archive = self.create_archive(owner=owner, private=True) |
712 | + self.assertEqual( |
713 | + private_archive.metadata_overrides, self.default_metadata_overrides |
714 | + ) |
715 | + overrides = {"Origin": "test_origin"} |
716 | + private_archive.setMetadataOverrides(overrides) |
717 | + self.assertEqual(private_archive.metadata_overrides, overrides) |
718 | + |
719 | + def test_admin_can_set_metadata_overrides_on_any_private_archive(self): |
720 | + private_archive = self.create_archive(private=True) |
721 | + login_celebrity("admin") |
722 | + self.assertEqual( |
723 | + private_archive.metadata_overrides, self.default_metadata_overrides |
724 | + ) |
725 | + overrides = {"Origin": "test_origin"} |
726 | + private_archive.setMetadataOverrides(overrides) |
727 | + self.assertEqual(private_archive.metadata_overrides, overrides) |
728 | + |
729 | + def test_anonymous_cannot_set_metadata_overrides_on_private_archive(self): |
730 | + private_archive = self.create_archive(private=True) |
731 | + login(ANONYMOUS) |
732 | + overrides = {"Origin": "test_origin"} |
733 | + self.assertRaises( |
734 | + Unauthorized, |
735 | + lambda: private_archive.setMetadataOverrides(overrides), |
736 | + ) |
737 | + |
738 | + def test_non_owner_cannot_set_metadata_overrides_on_private_archive(self): |
739 | + user = self.factory.makePerson() |
740 | + private_archive = self.create_archive(private=True) |
741 | + login_person(user) |
742 | + overrides = {"Origin": "test_origin"} |
743 | + self.assertRaises( |
744 | + Unauthorized, |
745 | + lambda: private_archive.setMetadataOverrides(overrides), |
746 | + ) |
747 | + |
748 | + def test_subscriber_cannot_set_metadata_overrides_on_private_archive(self): |
749 | + owner = self.factory.makePerson() |
750 | + user = self.factory.makePerson() |
751 | + private_archive = self.create_archive(owner=owner, private=True) |
752 | + with person_logged_in(owner): |
753 | + private_archive.newSubscription(user, owner) |
754 | + login_person(user) |
755 | + overrides = {"Origin": "test_origin"} |
756 | + self.assertRaises( |
757 | + Unauthorized, |
758 | + lambda: private_archive.setMetadataOverrides(overrides), |
759 | + ) |
760 | + |
761 | + def test_owner_can_set_metadata_overrides_on_own_primary_archive(self): |
762 | + owner = self.factory.makePerson() |
763 | + primary_archive = self.create_archive(owner=owner, primary=True) |
764 | + login_person(owner) |
765 | + self.assertEqual( |
766 | + primary_archive.metadata_overrides, self.default_metadata_overrides |
767 | + ) |
768 | + overrides = {"Origin": "test_origin"} |
769 | + primary_archive.setMetadataOverrides(overrides) |
770 | + self.assertEqual(primary_archive.metadata_overrides, overrides) |
771 | + |
772 | + def test_admin_can_set_metadata_overrides_on_any_primary_archive(self): |
773 | + primary_archive = self.create_archive(primary=True) |
774 | + login_celebrity("admin") |
775 | + self.assertEqual( |
776 | + primary_archive.metadata_overrides, self.default_metadata_overrides |
777 | + ) |
778 | + overrides = {"Origin": "test_origin"} |
779 | + primary_archive.setMetadataOverrides(overrides) |
780 | + self.assertEqual(primary_archive.metadata_overrides, overrides) |
781 | + |
782 | + def test_anonymous_cannot_set_metadata_overrides_on_primary_archive(self): |
783 | + primary_archive = self.create_archive(primary=True) |
784 | + login(ANONYMOUS) |
785 | + overrides = {"Origin": "test_origin"} |
786 | + self.assertRaises( |
787 | + Unauthorized, |
788 | + lambda: primary_archive.setMetadataOverrides(overrides), |
789 | + ) |
790 | + |
791 | + def test_non_owner_cannot_set_metadata_overrides_on_primary_archive(self): |
792 | + user = self.factory.makePerson() |
793 | + primary_archive = self.create_archive(primary=True) |
794 | + login_person(user) |
795 | + overrides = {"Origin": "test_origin"} |
796 | + self.assertRaises( |
797 | + Unauthorized, |
798 | + lambda: primary_archive.setMetadataOverrides(overrides), |
799 | + ) |
800 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
801 | index b4c9bb1..6cdf9b4 100644 |
802 | --- a/lib/lp/testing/factory.py |
803 | +++ b/lib/lp/testing/factory.py |
804 | @@ -3706,6 +3706,7 @@ class LaunchpadObjectFactory(ObjectFactory): |
805 | processors=None, |
806 | publishing_method=ArchivePublishingMethod.LOCAL, |
807 | repository_format=ArchiveRepositoryFormat.DEBIAN, |
808 | + metadata_overrides=None, |
809 | ): |
810 | """Create and return a new arbitrary archive. |
811 | |
812 | @@ -3765,6 +3766,7 @@ class LaunchpadObjectFactory(ObjectFactory): |
813 | processors=processors, |
814 | publishing_method=publishing_method, |
815 | repository_format=repository_format, |
816 | + metadata_overrides=metadata_overrides, |
817 | ) |
818 | |
819 | if private: |
LGTM! Just a very small suggestion.
I was wondering if there are any tests to update or add, but maybe not since this new model attribute isn't really used yet - we'll do it when adding APIs/UI.
Note that this cannot be merged until the DB change is deployed to production. I'm happy to approve this one, but I won't for now because of that :)