Skip to content

Commit 0f5b4c4

Browse files
committed
test: add github sync importer/webhook/etc tests
* fixed issue with re-imported releases not updating the release package info (downloadurl, tag name, etc.) * refactor metadata extractor for easier priority changes
1 parent 572e6e8 commit 0f5b4c4

File tree

6 files changed

+508
-26
lines changed

6 files changed

+508
-26
lines changed

django/library/github_integration.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,14 @@ def reimport_release(self, release) -> bool:
421421
):
422422
return False
423423

424+
package = release.imported_release_package
425+
package.name = self.github_release.get("tag_name")
426+
package.display_name = self.github_release.get("name", "")
427+
package.html_url = self.github_release.get("html_url", "")
428+
package.download_url = self.github_release.get("zipball_url", "")
429+
package.extra_data = self.github_release
430+
package.save()
431+
424432
return self._import_package_and_metadata(release)
425433

426434
def _resolve_tags(self, tag_names: list[str]) -> list:

django/library/jinja2/library/review/email/model_revisions_requested_imported.jinja

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Since this release is imported from GitHub, you will need to update it there and
1616
3. **Update the release tag:**
1717
- In the release edit form, you can choose which tag or branch the release should be associated with.
1818
- To include your latest changes, you should create a new tag. You can do this by typing a new version number in the "Tag version" box and selecting "Create new tag: [new_version] on publish". Make sure this new tag points to the branch with your recent commits.
19+
- Keep in mind that the original version number will be kept, so its recommended to append a suffix to the tag instead of incrementing, e.g. "v1.0.0-rev1"
1920
4. **Confirm the release has been updated:** Follow [this link]({{ build_absolute_uri(release.get_edit_url()) }}) to confirm that the release has been updated, making sure files are categorized and metadata is complete.
2021
5. **Notify reviewer:** Once changes have been made, use the "Notify Reviewer of Changes" button to send an alert to the reviewer.
2122

django/library/metadata.py

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -415,34 +415,36 @@ def _ensure_list(self, value):
415415
return []
416416
return [value]
417417

418-
def extract_license_spdx_id(self) -> str | None:
419-
# priority: github_repository.license.spdx_id > cff.license
418+
def _extract_license_from_github(self) -> str | None:
420419
gh_license = self._get_field(self.github_repository, "license")
421420
if isinstance(gh_license, dict):
422421
spdx_id = gh_license.get("spdx_id", None)
423422
if spdx_id:
424423
return spdx_id
425-
# cff.license should be an Enum with a valid spdx id (or a list of them)
424+
return None
425+
426+
def _extract_license_from_cff(self) -> str | None:
426427
cff_licenses = self._ensure_list(self._get_field(self.cff, "license"))
427428
for cff_license in cff_licenses:
428-
if cff_license and cff_license.value:
429+
if cff_license and hasattr(cff_license, "value"):
429430
return cff_license.value
431+
return None
430432

431-
def extract_release_notes(self) -> str | None:
432-
# priority: codemeta.releaseNotes > github_release.body
433-
# codemeta.releaseNotes can be a list, make sure its a str, take the first one
433+
def _extract_release_notes_from_codemeta(self) -> str | None:
434434
release_notes = self._get_field(self.codemeta, "releaseNotes")
435435
if release_notes:
436436
if isinstance(release_notes, list):
437437
release_notes = release_notes[0]
438438
return release_notes if isinstance(release_notes, str) else None
439439
elif isinstance(release_notes, str):
440440
return release_notes
441+
return None
442+
443+
def _extract_release_notes_from_github(self) -> str | None:
441444
body = self._get_field(self.github_release, "body")
442445
return body if isinstance(body, str) else None
443446

444-
def extract_os(self) -> str:
445-
# priority: codemeta.operatingSystem
447+
def _extract_os_from_codemeta(self) -> str:
446448
codemeta_os = self._get_field(self.codemeta, "operatingSystem")
447449
if not codemeta_os:
448450
return ""
@@ -467,8 +469,7 @@ def extract_os(self) -> str:
467469
return "other"
468470
return ""
469471

470-
def extract_programming_languages(self) -> list[str] | None:
471-
# priority: codemeta.programmingLanguage > [github_repository.language]
472+
def _extract_programming_languages_from_codemeta(self) -> list[str] | None:
472473
codemeta_langs = self._ensure_list(
473474
self._get_field(self.codemeta, "programmingLanguage")
474475
)
@@ -480,15 +481,15 @@ def extract_programming_languages(self) -> list[str] | None:
480481
lang_name = self._get_field(codemeta_lang, "name")
481482
if lang_name:
482483
langs.append(lang_name)
483-
if langs:
484-
return langs
485-
else:
486-
gh_lang = self._get_field(self.github_repository, "language")
487-
if gh_lang and isinstance(gh_lang, str):
488-
return [gh_lang]
484+
return langs if langs else None
485+
486+
def _extract_programming_languages_from_github(self) -> list[str] | None:
487+
gh_lang = self._get_field(self.github_repository, "language")
488+
if gh_lang and isinstance(gh_lang, str):
489+
return [gh_lang]
490+
return None
489491

490-
def extract_platforms(self) -> list[str] | None:
491-
# priority: codemeta.runtimePlatform
492+
def _extract_platforms_from_codemeta(self) -> list[str] | None:
492493
runtime_platforms = self._ensure_list(
493494
self._get_field(self.codemeta, "runtimePlatform")
494495
)
@@ -501,12 +502,25 @@ def extract_platforms(self) -> list[str] | None:
501502
def convert(self) -> dict:
502503
"""return a dictionary with the metadata fields for a codebase release from
503504
given sources"""
505+
# set priority
506+
license_spdx_id = (
507+
self._extract_license_from_github() or self._extract_license_from_cff()
508+
)
509+
release_notes = (
510+
self._extract_release_notes_from_codemeta()
511+
or self._extract_release_notes_from_github()
512+
)
513+
programming_languages = (
514+
self._extract_programming_languages_from_codemeta()
515+
or self._extract_programming_languages_from_github()
516+
)
517+
504518
return {
505-
"license_spdx_id": self.extract_license_spdx_id(),
506-
"release_notes": self.extract_release_notes(),
507-
"os": self.extract_os(),
508-
"programming_languages": self.extract_programming_languages(),
509-
"platforms": self.extract_platforms(),
519+
"license_spdx_id": license_spdx_id,
520+
"release_notes": release_notes,
521+
"os": self._extract_os_from_codemeta(),
522+
"programming_languages": programming_languages,
523+
"platforms": self._extract_platforms_from_codemeta(),
510524
}
511525

512526

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
from unittest.mock import patch, MagicMock
2+
from django.test import TestCase
3+
4+
from core.tests.base import UserFactory
5+
from library.github_integration import (
6+
GitHubRepoValidator,
7+
GitHubReleaseImporter,
8+
)
9+
from library.models import (
10+
CodebaseGitRemote,
11+
GithubIntegrationAppInstallation,
12+
CodebaseRelease,
13+
)
14+
from library.tests.base import CodebaseFactory
15+
16+
SAMPLE_PAYLOAD = {
17+
"action": "released",
18+
"release": {
19+
"id": 12345,
20+
"tag_name": "v1.0.0",
21+
"name": "Version 1.0.0",
22+
"body": "Initial release",
23+
"draft": False,
24+
"prerelease": False,
25+
"zipball_url": "https://api.github.com/repos/testuser/test-repo/zipball/v1.0.0",
26+
"html_url": "https://github.com/testuser/test-repo/releases/tag/v1.0.0",
27+
},
28+
"repository": {
29+
"name": "test-repo",
30+
"owner": {"login": "testuser"},
31+
"private": False,
32+
},
33+
"installation": {"id": 54321},
34+
}
35+
36+
37+
class GitHubRepoValidatorTests(TestCase):
38+
def setUp(self):
39+
# set up a user and installation for testing validator methods
40+
self.user = UserFactory().create()
41+
self.installation = GithubIntegrationAppInstallation.objects.create(
42+
user=self.user,
43+
github_login="testuser",
44+
installation_id=1,
45+
github_user_id=123,
46+
)
47+
48+
def test_validate_format(self):
49+
# test valid repo names
50+
for name in ["repo", "repo-1", "repo.1", "my_repo"]:
51+
with self.subTest(name=name):
52+
validator = GitHubRepoValidator(name)
53+
self.assertIsNone(validator.validate_format())
54+
55+
# test invalid repo names
56+
for name in [
57+
"repo with space",
58+
"repo!",
59+
"a" * 101,
60+
"repo.git",
61+
"contains-github",
62+
]:
63+
with self.subTest(name=name):
64+
validator = GitHubRepoValidator(name)
65+
with self.assertRaises(ValueError):
66+
validator.validate_format()
67+
68+
69+
class GitHubReleaseImporterTests(TestCase):
70+
def setUp(self):
71+
# set up a user, codebase, and remote for importing releases
72+
self.user = UserFactory().create()
73+
self.installation = GithubIntegrationAppInstallation.objects.create(
74+
user=self.user,
75+
github_login="testuser",
76+
installation_id=54321,
77+
github_user_id=123,
78+
)
79+
self.codebase = CodebaseFactory(submitter=self.user).create()
80+
mirror = self.codebase.create_git_mirror()
81+
self.remote = CodebaseGitRemote.objects.create(
82+
mirror=mirror,
83+
owner="testuser",
84+
repo_name="test-repo",
85+
is_user_repo=True,
86+
should_import=True,
87+
)
88+
self.payload = SAMPLE_PAYLOAD.copy()
89+
90+
def test_init_success(self):
91+
# should initialize successfully with a valid payload
92+
importer = GitHubReleaseImporter(self.payload)
93+
self.assertEqual(importer.github_release_id, "12345")
94+
self.assertEqual(importer.codebase, self.codebase)
95+
self.assertTrue(importer.is_new_github_release)
96+
97+
def test_init_failures(self):
98+
# test various invalid payloads that should raise ValueError
99+
test_cases = {
100+
"draft": ("release", "draft", True),
101+
"prerelease": ("release", "prerelease", True),
102+
"private": ("repository", "private", True),
103+
"wrong_action": ("action", None, "created"),
104+
"no_remote": ("repository", "name", "non-existent-repo"),
105+
}
106+
for name, (key1, key2, value) in test_cases.items():
107+
with self.subTest(name=name):
108+
bad_payload = self.payload.copy()
109+
if key2:
110+
bad_payload[key1] = bad_payload[key1].copy()
111+
bad_payload[key1][key2] = value
112+
else:
113+
bad_payload[key1] = value
114+
with self.assertRaises(ValueError):
115+
GitHubReleaseImporter(bad_payload)
116+
117+
def test_extract_semver(self):
118+
# test semantic version extraction
119+
importer = GitHubReleaseImporter(self.payload)
120+
self.assertEqual(importer.extract_semver("v1.2.3"), "1.2.3")
121+
self.assertEqual(importer.extract_semver("1.2.3"), "1.2.3")
122+
self.assertEqual(importer.extract_semver("version 1.2.3-beta"), "1.2.3")
123+
self.assertIsNone(importer.extract_semver("1.2"))
124+
self.assertIsNone(importer.extract_semver("invalid-version"))
125+
126+
@patch("library.models.CodebaseRelease.get_fs_api")
127+
@patch("library.github_integration.GitHubApi.get_user_installation_access_token")
128+
def test_import_new_release(self, mock_get_token, mock_get_fs_api):
129+
# mock token and fs_api calls
130+
mock_get_token.return_value = "fake-token"
131+
mock_fs_api = MagicMock()
132+
mock_fs_api.import_release_package.return_value = ({}, {}) # codemeta, cff
133+
mock_get_fs_api.return_value = mock_fs_api
134+
135+
# import a new release
136+
importer = GitHubReleaseImporter(self.payload)
137+
success = importer.import_new_release()
138+
139+
# check that it was successful and objects were created
140+
self.assertTrue(success)
141+
self.assertTrue(
142+
CodebaseRelease.objects.filter(
143+
codebase=self.codebase, version_number="1.0.0"
144+
).exists()
145+
)
146+
release = CodebaseRelease.objects.get(version_number="1.0.0")
147+
self.assertEqual(release.imported_release_package.uid, "12345")
148+
self.assertEqual(release.submitter, self.codebase.submitter)
149+
mock_fs_api.import_release_package.assert_called_once()
150+
151+
@patch("library.models.CodebaseRelease.get_fs_api")
152+
@patch("library.github_integration.GitHubApi.get_user_installation_access_token")
153+
def test_reimport_release(self, mock_get_token, mock_get_fs_api):
154+
# mock token and fs_api calls
155+
mock_get_token.return_value = "fake-token"
156+
mock_fs_api = MagicMock()
157+
mock_fs_api.import_release_package.return_value = ({}, {}) # codemeta, cff
158+
mock_get_fs_api.return_value = mock_fs_api
159+
160+
# first, import a new release
161+
importer = GitHubReleaseImporter(self.payload)
162+
importer.import_or_reimport()
163+
164+
self.assertEqual(CodebaseRelease.objects.count(), 1)
165+
release = CodebaseRelease.objects.first()
166+
self.assertEqual(
167+
release.imported_release_package.download_url,
168+
"https://api.github.com/repos/testuser/test-repo/zipball/v1.0.0",
169+
)
170+
171+
# now, create a new importer with an updated payload for an "edited" event
172+
reimport_payload = self.payload.copy()
173+
reimport_payload["action"] = "edited"
174+
reimport_payload["release"] = reimport_payload["release"].copy()
175+
new_url = (
176+
"https://api.github.com/repos/testuser/test-repo/zipball/v1.0.0-updated"
177+
)
178+
reimport_payload["release"]["zipball_url"] = new_url
179+
180+
importer2 = GitHubReleaseImporter(reimport_payload)
181+
success = importer2.import_or_reimport()
182+
183+
# assert that the re-import was successful and the release was updated
184+
self.assertTrue(success)
185+
self.assertEqual(mock_fs_api.import_release_package.call_count, 2)
186+
187+
release.refresh_from_db()
188+
self.assertEqual(release.imported_release_package.download_url, new_url)
189+
190+
self.remote.refresh_from_db()
191+
self.assertIn("Successfully re-imported", self.remote.last_import_log)
192+
193+
# release version number should NOT have changed
194+
self.assertEqual(release.version_number, "1.0.0")
195+
196+
# re-importing with no change does nothing
197+
importer3 = GitHubReleaseImporter(reimport_payload)
198+
success_no_change = importer3.import_or_reimport()
199+
self.assertFalse(success_no_change)
200+
# should still be 2, not called again
201+
self.assertEqual(mock_fs_api.import_release_package.call_count, 2)
202+
203+
# re-importing a published release does nothing
204+
release.status = CodebaseRelease.Status.PUBLISHED
205+
release.save()
206+
published_reimport_payload = reimport_payload.copy()
207+
published_reimport_payload["release"][
208+
"zipball_url"
209+
] = "https://another.url/zipball.zip"
210+
importer4 = GitHubReleaseImporter(published_reimport_payload)
211+
success_published = importer4.import_or_reimport()
212+
self.assertFalse(success_published)
213+
# fs_api should not be called again
214+
self.assertEqual(mock_fs_api.import_release_package.call_count, 2)
215+
self.remote.refresh_from_db()
216+
self.assertIn("Release already exists", self.remote.last_import_log)
217+
218+
# re-importing an under_review release should work
219+
release.status = CodebaseRelease.Status.UNDER_REVIEW
220+
release.save()
221+
review_reimport_payload = self.payload.copy()
222+
review_reimport_payload["action"] = "edited"
223+
review_reimport_payload["release"] = review_reimport_payload["release"].copy()
224+
review_url = (
225+
"https://api.github.com/repos/testuser/test-repo/zipball/v1.0.0-review"
226+
)
227+
review_reimport_payload["release"]["zipball_url"] = review_url
228+
importer5 = GitHubReleaseImporter(review_reimport_payload)
229+
success_review = importer5.import_or_reimport()
230+
self.assertTrue(success_review)
231+
self.assertEqual(mock_fs_api.import_release_package.call_count, 3)
232+
release.refresh_from_db()
233+
self.assertEqual(release.imported_release_package.download_url, review_url)

0 commit comments

Comments
 (0)