import json
import re
import zoneinfo
from datetime import UTC, date, datetime
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.template.loader import render_to_string
from django.test import RequestFactory, TestCase, override_settings
from django.urls import reverse
from django.utils.timezone import make_aware
from checklists.models import (
SEVERITY_LEVELS_DOCS,
BugFixRelease,
FeatureRelease,
PreRelease,
Releaser,
SecurityIssue,
SecurityIssueReleasesThrough,
SecurityRelease,
get_cvss_severity,
)
from checklists.tests.factory import Factory
class BaseChecklistTestCaseMixin:
checklist_class = None
factory = Factory()
request_factory = RequestFactory()
def debug_checklist(self, content):
fname = f"{self.id()}-checklist.md"
with open(fname, "w") as f:
f.write(content)
return fname
def make_checklist(self, releaser=None, when=None, **kwargs):
return self.factory.make_checklist(
self.checklist_class, releaser=releaser, when=when, **kwargs
)
def assertInChecklistContent(self, text, content, flat=False):
"""Like assertIn, but without worrying about whitespaces."""
if flat:
# Collapse all whitespace to single spaces.
content = re.sub(r"\s+", " ", content)
if text not in content:
fname = self.debug_checklist(content)
self.fail(
f"The given:\n{text}\nis not in the checklist content, which was "
f"stored in {fname} for further debugging."
)
def assertNotInChecklistContent(self, text, content):
"""Show more readable error message on `assertNotIn` failures."""
idx = content.find(text)
if idx != -1:
start = max(idx - 10, 0)
end = min(start + 100, len(content))
fragment = content[start:end]
self.fail(f"{text!r} unexpectedly found in:\n{fragment}")
def assertStubReleaseNotesAdded(self, release, content):
expected = render_to_string(
"checklists/_stub_release_notes.md", {"release": release}
)
self.assertIn(expected, content)
def assertMakeReleasePublicAdded(self, release, content):
expected = render_to_string(
"checklists/_make_release_public.md", {"release": release}
)
self.assertIn(expected, content)
version = release.version
data = [
"- [ ] Add (or edit if existing) the the [release entry in the admin]"
f"(https://www.djangoproject.com/admin/releases/release?version={version})",
"- Is active: False",
f"- LTS: {release.is_lts}",
f"- Release date: {release.date.isoformat()}",
f"- `VERSION={version} scripts/verify_release.sh`",
"- `twine upload --repository django dist/*`",
'- [ ] Mark the release as "active" in\n '
f"https://www.djangoproject.com/admin/releases/release/{version}/change/",
]
for item in data:
with self.subTest(item=item):
self.assertIn(item, expected)
def assertPushAndAnnouncesAdded(self, instance, content):
expected = render_to_string(
"checklists/_push_changes_and_announce.md",
context={
"instance": instance,
"releaser": instance.releaser,
"slug": instance.slug,
"version": instance.version,
"title": instance.__class__.__name__,
**instance.__dict__,
},
)
self.assertIn(expected, content)
data = [
"- `git push -v`",
"- `git push --tags`",
]
for item in data:
with self.subTest(item=item):
self.assertIn(item, expected)
@override_settings(
TEMPLATES=[
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [],
"string_if_invalid": "INVALID: %s",
},
}
]
)
def do_render_checklist(self, checklist_instance=None):
if checklist_instance is None:
checklist_instance = self.make_checklist()
request = self.request_factory.get("/")
content = checklist_instance.render_to_string(request)
self.assertNotInChecklistContent("INVALID", content)
return content
class BugFixReleaseChecklistTestCase(BaseChecklistTestCaseMixin, TestCase):
checklist_class = BugFixRelease
def test_blogpost_properties(self):
release = self.factory.make_release(version="5.2.4")
checklist = self.make_checklist(release=release)
self.assertEqual(checklist.slug, "bugfix-releases")
self.assertEqual(
checklist.blogpost_title, "Django bugfix release issued: 5.2.4"
)
self.assertEqual(
checklist.blogpost_summary,
"Today the Django project issued a bugfix release for the 5.2 release "
"series.",
)
self.assertEqual(
checklist.blogpost_template, "checklists/release_bugfix_blogpost.md"
)
def test_render_checklist(self):
release = self.factory.make_release(version="5.2.4")
checklist_instance = self.make_checklist(release=release)
checklist_content = self.do_render_checklist(checklist_instance)
self.assertPushAndAnnouncesAdded(checklist_instance, checklist_content)
self.assertStubReleaseNotesAdded(release, checklist_content)
self.assertMakeReleasePublicAdded(release, checklist_content)
class SecurityReleaseChecklistTestCase(BaseChecklistTestCaseMixin, TestCase):
checklist_class = SecurityRelease
def make_checklist(self, releaser=None, when=None, **kwargs):
return self.factory.make_security_checklist(
releaser=releaser, when=when, **kwargs
)
def test_affected_releases(self):
release51 = self.factory.make_release(version="5.1.8")
release52 = self.factory.make_release(version="5.2")
prerelease = self.factory.make_release(version="6.0a1")
checklist = self.make_checklist(releases=[release51, release52, prerelease])
self.assertEqual(
checklist.affected_releases, [prerelease, release52, release51]
)
def test_blogpost_info(self):
release42 = self.factory.make_release(version="4.2.13")
release51 = self.factory.make_release(version="5.1.7")
release52 = self.factory.make_release(version="5.2")
prerelease = self.factory.make_release(version="6.0a1")
# Test proper use of Oxford comma.
for releases, expected, verb in [
([release52], "5.2", "fixes"),
([release52, prerelease], "5.2", "fixes"),
([release52, release51, prerelease], "5.2 and 5.1.7", "fix"),
(
[release52, release51, release42, prerelease],
"5.2, 5.1.7, and 4.2.13",
"fix",
),
]:
with self.subTest(releases=releases):
checklist = self.make_checklist(releases=releases)
self.assertEqual(
checklist.blogpost_title,
f"Django security releases issued: {expected}",
)
self.assertEqual(
checklist.blogpost_summary,
f"Django {expected} {verb} one security issue",
)
def test_blogpost_info_two_issues(self):
release51 = self.factory.make_release(version="5.1.9")
release52 = self.factory.make_release(version="5.2")
prerelease = self.factory.make_release(version="6.0a1")
checklist = self.make_checklist(releases=[release51, release52, prerelease])
self.factory.make_security_issue(checklist, releases=[release52])
self.assertEqual(
checklist.blogpost_template, "checklists/release_security_blogpost.md"
)
self.assertEqual(
checklist.blogpost_summary, "Django 5.2 and 5.1.9 fix 2 security issues"
)
def test_versions(self):
release51 = self.factory.make_release(version="5.1.9")
release52 = self.factory.make_release(version="5.2")
prerelease = self.factory.make_release(version="6.0a1")
checklist = self.make_checklist(releases=[release51, release52, prerelease])
self.assertEqual(checklist.version, "5.2 and 5.1.9")
self.assertEqual(checklist.versions, ["5.2", "5.1.9"])
def test_latest_release(self):
release51 = self.factory.make_release(version="5.1.9")
release52 = self.factory.make_release(version="5.2")
prerelease = self.factory.make_release(version="6.0a1")
checklist = self.make_checklist(releases=[release51, release52, prerelease])
self.assertEqual(checklist.latest_release, release52)
def test_tags(self):
release42 = self.factory.make_release(version="4.2.13")
release51 = self.factory.make_release(version="5.1.9")
release52 = self.factory.make_release(version="5.2")
prerelease = self.factory.make_release(version="6.0a1")
checklist = self.make_checklist(
releases=[release42, release51, release52, prerelease]
)
self.assertEqual(checklist.tags, ["security", "5-2", "5-1", "4-2"])
def test_affected_branches(self):
release51 = self.factory.make_release(version="5.1.9")
release52 = self.factory.make_release(version="5.2")
prerelease_alpha = self.factory.make_release(version="6.0a1")
prerelease_rc = self.factory.make_release(version="5.2rc1")
checklist = self.make_checklist(
releases=[release51, release52, prerelease_alpha, prerelease_rc]
)
self.assertEqual(
checklist.affected_branches,
[
"main",
"6.0 (currently at alpha status)",
"5.2 (currently at release candidate status)",
"5.2",
"5.1",
],
)
def test_cnas(self):
release51 = self.factory.make_release(version="5.1.9")
release52 = self.factory.make_release(version="5.2")
checklist = self.make_checklist(releases=[])
self.factory.make_security_issue(
checklist,
[release51, release52],
cve_year_number="CVE-2025-11111",
cna="MITRE",
)
self.factory.make_security_issue(
checklist, [release52], cve_year_number="CVE-2025-22222", cna="DSF"
)
self.assertEqual(list(checklist.cnas), ["MITRE", "DSF"])
def test_hashes_by_versions(self):
release51 = self.factory.make_release(version="5.1.9")
release52 = self.factory.make_release(version="5.2")
checklist = self.make_checklist(releases=[])
issue1 = self.factory.make_security_issue(
checklist,
[release51, release52],
cve_year_number="CVE-2025-11111",
commit_hash_main="abc111main",
)
issue2 = self.factory.make_security_issue(
checklist,
[release52],
cve_year_number="CVE-2025-22222",
commit_hash_main="abc222main",
)
SecurityIssueReleasesThrough.objects.filter(
securityissue=issue1, release=release51
).update(commit_hash="hash51issue1")
SecurityIssueReleasesThrough.objects.filter(
securityissue=issue1, release=release52
).update(commit_hash="hash52issue1")
SecurityIssueReleasesThrough.objects.filter(
securityissue=issue2, release=release52
).update(commit_hash="hash52issue2")
hashes = checklist.hashes_by_versions
expected = [
{"branch": "5.2", "cve": "CVE-2025-11111", "hash": "hash52issue1"},
{"branch": "5.1", "cve": "CVE-2025-11111", "hash": "hash51issue1"},
{"branch": "5.2", "cve": "CVE-2025-22222", "hash": "hash52issue2"},
{"branch": "main", "cve": "CVE-2025-11111", "hash": "abc111main"},
{"branch": "main", "cve": "CVE-2025-22222", "hash": "abc222main"},
]
self.assertEqual(hashes, expected)
def test_no_issues(self):
checklist = self.make_checklist(releases=[])
self.assertEqual(list(checklist.cves), [])
self.assertEqual(list(checklist.cnas), [])
self.assertEqual(checklist.hashes_by_versions, [])
def test_cves_numeric_sort_order_on_cve_year_number(self):
release = self.factory.make_release(version="5.2")
checklist = self.make_checklist(releases=[])
# Lexicographic sort would put 10000 before 9999 ("1" < "9").
issue_9999 = self.factory.make_security_issue(
checklist, [release], cve_year_number="CVE-2025-9999"
)
issue_10000 = self.factory.make_security_issue(
checklist, [release], cve_year_number="CVE-2025-10000"
)
self.assertEqual(checklist.cves, [issue_9999, issue_10000])
def test_hashes_by_versions_numeric_sort_order_on_cve_year_number(self):
release = self.factory.make_release(version="5.2")
checklist = self.make_checklist(releases=[])
# Lexicographic sort would put 10000 before 9999 ("1" < "9").
self.factory.make_security_issue(
checklist,
[release],
cve_year_number="CVE-2025-9999",
commit_hash_main="main9999",
)
self.factory.make_security_issue(
checklist,
[release],
cve_year_number="CVE-2025-10000",
commit_hash_main="main10000",
)
self.assertEqual(
[h["cve"] for h in checklist.hashes_by_versions],
["CVE-2025-9999", "CVE-2025-10000", "CVE-2025-9999", "CVE-2025-10000"],
)
def test_render_checklist_simple(self):
checklist = self.make_checklist()
checklist_content = self.do_render_checklist(checklist)
self.assertIn("- [ ] Submit CVE IDs Request for all issues", checklist_content)
with self.subTest(task="One Week before steps"):
self.assertIn("## One Week before", checklist_content)
self.assertIn(
"- [ ] Land the stub release notes and release date updates in main "
"and 5.2",
checklist_content,
)
with self.subTest(task="Stub release notes added"):
self.assertStubReleaseNotesAdded(
checklist.latest_release, checklist_content
)
with self.subTest(task="Make release public steps added"):
self.assertMakeReleasePublicAdded(
checklist.latest_release, checklist_content
)
with self.subTest(task="Push and announce steps added"):
self.assertPushAndAnnouncesAdded(checklist, checklist_content)
with self.subTest(task="Update HackerOne"):
self.assertInChecklistContent(
"Close open report(s) in HackerOne", checklist_content
)
self.assertInChecklistContent(
"Disclose report(s) in HackerOne", checklist_content
)
with self.subTest(task="Write blogpost author and active status"):
releaser_name = checklist.releaser.user.get_full_name()
self.assertIn(f"- Author: `{releaser_name}`", checklist_content)
self.assertIn("- Active: `False`", checklist_content)
def test_render_checklist_affects_prerelease(self):
releases = [
self.factory.make_release(version="5.0.14", date=date(2025, 4, 2)),
self.factory.make_release(version="5.1.8", date=date(2025, 4, 2)),
self.factory.make_release(version="5.2rc1", date=date(2025, 3, 19)),
]
tz = zoneinfo.ZoneInfo("America/Chicago")
when = datetime(2025, 5, 7, 11, 18, 23, tzinfo=tz)
checklist = self.make_checklist(releases=[], when=when)
self.factory.make_security_issue(
checklist, releases, cve_year_number="CVE-2025-11111"
)
self.factory.make_security_issue(
checklist, releases, cve_year_number="CVE-2025-22222"
)
checklist_content = self.do_render_checklist(checklist)
self.assertNotInChecklistContent("5.2 before 5.2rc1", checklist_content)
self.assertIn(
"- Affected product(s)/code base (SPLIT in product and version (X before Y)"
" in rows!):",
checklist_content,
)
for release in ("5.1 before 5.1.8", "5.0 before 5.0.14"):
with self.subTest(release=release):
expected = f"[row 1] Django [row 2] {release}"
self.assertInChecklistContent(expected, checklist_content, flat=True)
cves = checklist.securityissue_set.all()
prenotification = [
"Create a new text file `prenotification-email.txt` with content",
"a set of security releases will be issued on Wednesday, May 7, 2025 "
"around 16:18 UTC",
*(cve.headline_for_blogpost for cve in cves),
"## Affected supported versions "
+ " ".join(f"* Django {branch}" for branch in checklist.affected_branches),
"* Django 5.0.14",
"* Django 5.1.8",
]
for detail in prenotification:
with self.subTest(detail=detail):
self.assertInChecklistContent(detail, checklist_content, flat=True)
announce = [
"Wednesday, May 7 around 16:18 UTC.",
"They will fix 2 security defects",
]
for detail in announce:
with self.subTest(detail=detail):
self.assertInChecklistContent(detail, checklist_content, flat=True)
def test_render_checklist_cve_record_url(self):
release = self.factory.make_release(version="5.2.1")
checklist = self.make_checklist(releases=[])
issue = self.factory.make_security_issue(
checklist,
[release],
cve_year_number="CVE-2025-11111",
cna="DSF",
)
checklist_content = self.do_render_checklist(checklist)
expected_url = reverse(
"checklists:cve_json_record", args=[issue.cve_year_number]
)
self.assertIn(f"Get CVE Record from {expected_url}", checklist_content)
def test_render_checklist_blogdescription_display(self):
checklist = self.make_checklist(releases=[])
blog = (
"This is a blog description that would be used in the Django site "
'"News" section. The full list of news can be found `in this link '
"
An issue was discovered in 5.0 before 5.0.14 and 5.1 before 5.1.8.
" f"{cve_description}
" "Earlier, unsupported Django series (such as 5.0.x, 4.1.x, and 3.2.x) " "were not evaluated and may also be affected.
" f"Django would like to thank {reporter} for reporting this issue.
" ) expected = [ ("affected", affected_versions), ("credits", credits), ("datePublic", checklist.when.isoformat()), ( "descriptions", [ { "lang": "en", "value": expected_description, "supportingMedia": [ { "type": "text/html", "base64": False, "value": expected_html_description, }, ], }, ], ), ( "metrics", [ { "other": { "content": { "namespace": SEVERITY_LEVELS_DOCS, "value": "moderate", }, "type": "Django severity rating", } } ], ), ("references", references), ( "timeline", [ { "lang": "en", "time": checklist.when.isoformat(), "value": "Security release issued.", }, ], ), ("title", cve_summary), ] cve_data = issue.cve_data for key, value in expected: with self.subTest(key=key): self.assertEqual(cve_data.get(key), value) cve_json = json.dumps(cve_data, sort_keys=True, indent=2) with self.subTest(key="json"): self.assertIn(cve_json, checklist_content) def test_cve_data_with_timeline(self): releases = [ self.factory.make_release(version="5.1.8", date=date(2025, 4, 2)), ] when = datetime(2025, 4, 2, 14, 30, tzinfo=UTC) reported = datetime(2025, 2, 15, 10, 0, tzinfo=UTC) confirmed = datetime(2025, 3, 1, 16, 30, tzinfo=UTC) checklist = self.make_checklist(releases=[], when=when) issue = self.factory.make_security_issue( checklist, releases, cve_year_number="CVE-2025-12345", reporter="Alice Reporter", remediator="Bob Fixer", reported_at=reported, confirmed_at=confirmed, ) cve_data = issue.cve_data self.assertIn("timeline", cve_data) timeline = cve_data["timeline"] self.assertEqual(len(timeline), 3) self.assertEqual( timeline[0], { "lang": "en", "time": reported.isoformat(), "value": "Initial report received.", }, ) self.assertEqual( timeline[1], { "lang": "en", "time": confirmed.isoformat(), "value": "Vulnerability confirmed.", }, ) self.assertEqual( timeline[2], { "lang": "en", "time": when.isoformat(), "value": "Security release issued.", }, ) credits = cve_data["credits"] self.assertEqual(len(credits), 3) self.assertEqual( credits[0], {"lang": "en", "type": "reporter", "value": "Alice Reporter"} ) self.assertEqual( credits[1], {"lang": "en", "type": "remediation developer", "value": "Bob Fixer"}, ) self.assertEqual( credits[2], { "lang": "en", "type": "coordinator", "value": checklist.releaser.user.get_full_name(), }, ) def test_hashes_by_branch(self): releases = [ self.factory.make_release(version="5.0.14"), self.factory.make_release(version="5.1.8"), ] checklist = self.make_checklist(releases=[]) issue = self.factory.make_security_issue( checklist, releases, commit_hash_main="abc123main" ) SecurityIssueReleasesThrough.objects.filter( securityissue=issue, release__version="5.0.14" ).update(commit_hash="def456branch50") SecurityIssueReleasesThrough.objects.filter( securityissue=issue, release__version="5.1.8" ).update(commit_hash="ghi789branch51") hashes = issue.hashes_by_branch self.assertEqual( hashes, [ ("main", "abc123main"), ("5.1", "ghi789branch51"), ("5.0", "def456branch50"), ], ) class SecurityIssueReleaseThroughTestCase(TestCase): factory = Factory() def test_unique_constraint_securityissue_release(self): release = self.factory.make_release(version="5.2") checklist = self.factory.make_security_checklist(releases=[]) issue = self.factory.make_security_issue(checklist, [release]) with self.assertRaises(IntegrityError): SecurityIssueReleasesThrough.objects.create( securityissue=issue, release=release ) def test_unique_constraint_commit_hash(self): release1 = self.factory.make_release(version="5.1.8") release2 = self.factory.make_release(version="5.2") checklist = self.factory.make_security_checklist(releases=[]) issue = self.factory.make_security_issue(checklist, [release1, release2]) SecurityIssueReleasesThrough.objects.filter( securityissue=issue, release=release1 ).update(commit_hash="abc123") through2 = SecurityIssueReleasesThrough.objects.get( securityissue=issue, release=release2 ) through2.commit_hash = "abc123" with self.assertRaises(IntegrityError): through2.save() def test_empty_commit_hash_allowed_multiple_times(self): release1 = self.factory.make_release(version="5.1.8") release2 = self.factory.make_release(version="5.2") checklist = self.factory.make_security_checklist(releases=[]) issue = self.factory.make_security_issue(checklist, [release1, release2]) through1 = SecurityIssueReleasesThrough.objects.get( securityissue=issue, release=release1 ) through2 = SecurityIssueReleasesThrough.objects.get( securityissue=issue, release=release2 ) self.assertEqual(through1.commit_hash, "") self.assertEqual(through2.commit_hash, "") class GetCvssSeverityTests(TestCase): def test_none_for_null_score(self): self.assertEqual(get_cvss_severity(None), "NONE") def test_none_for_zero_score(self): self.assertEqual(get_cvss_severity(0), "NONE") def test_low(self): self.assertEqual(get_cvss_severity(0.1), "LOW") self.assertEqual(get_cvss_severity(3.9), "LOW") def test_medium(self): self.assertEqual(get_cvss_severity(4.0), "MEDIUM") self.assertEqual(get_cvss_severity(6.9), "MEDIUM") def test_high(self): self.assertEqual(get_cvss_severity(7.0), "HIGH") self.assertEqual(get_cvss_severity(8.9), "HIGH") def test_critical(self): self.assertEqual(get_cvss_severity(9.0), "CRITICAL") self.assertEqual(get_cvss_severity(10.0), "CRITICAL") class SecurityIssueTestCase(TestCase): def test_cve_year_number_invalid(self): invalid_cve_numbers = [ "CVE-2026-1", "CVE-2026-XXXX", "CVE-20026-1111", ] for cve in invalid_cve_numbers: with self.subTest(cve=cve): with self.assertRaises(ValidationError) as context: SecurityIssue(cve_year_number=cve).full_clean() self.assertEqual( context.exception.message_dict.get("cve_year_number"), ["Enter a valid value."], ) def test_cve_year_number_valid(self): valid_cve_numbers = [ "CVE-2026-12345", "CVE-2026-1234", ] for cve in valid_cve_numbers: with self.subTest(cve=cve): # No ValidationError raised. SecurityIssue(cve_year_number=cve).full_clean( exclude=( "summary", "description", "cve_type", "impact", "confirmed_at", "reported_at", ) ) class CvssMetricsInCveDataTests(TestCase): factory = Factory() def _make_issue(self, **kwargs): checklist = self.factory.make_security_checklist(releases=[]) return self.factory.make_security_issue(checklist, **kwargs) def _get_metrics(self, issue): return issue.cve_data["metrics"] def test_only_django_severity_when_no_cvss(self): issue = self._make_issue() metrics = self._get_metrics(issue) self.assertEqual(len(metrics), 1) self.assertIn("other", metrics[0]) self.assertEqual(metrics[0]["other"]["type"], "Django severity rating") def test_v3_added_when_both_v3_fields_set(self): issue = self._make_issue( cvss_v3_vector_string="CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", cvss_v3_score="7.5", ) metrics = self._get_metrics(issue) self.assertEqual(len(metrics), 2) cvss = metrics[1]["cvssV3_1"] self.assertEqual(cvss["version"], "3.1") self.assertEqual( cvss["vectorString"], "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", ) self.assertEqual(cvss["baseScore"], 7.5) self.assertEqual(cvss["baseSeverity"], "HIGH") def test_v4_added_when_both_v4_fields_set(self): vector = "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" issue = self._make_issue(cvss_v4_vector_string=vector, cvss_v4_score="8.7") metrics = self._get_metrics(issue) self.assertEqual(len(metrics), 2) cvss = metrics[1]["cvssV4_0"] self.assertEqual(cvss["version"], "4.0") self.assertEqual(cvss["vectorString"], vector) self.assertEqual(cvss["baseScore"], 8.7) self.assertEqual(cvss["baseSeverity"], "HIGH") def test_both_v3_and_v4_added(self): vector_v3 = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" vector_v4 = "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" issue = self._make_issue( cvss_v3_vector_string=vector_v3, cvss_v3_score="7.5", cvss_v4_vector_string=vector_v4, cvss_v4_score="8.7", ) metrics = self._get_metrics(issue) self.assertEqual(len(metrics), 3) self.assertIn("other", metrics[0]) self.assertIn("cvssV3_1", metrics[1]) self.assertIn("cvssV4_0", metrics[2]) def test_v3_omitted_when_score_missing(self): vector = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N" issue = self._make_issue(cvss_v3_vector_string=vector) metrics = self._get_metrics(issue) self.assertEqual(len(metrics), 1) self.assertNotIn("cvssV3_1", metrics[0]) def test_v3_omitted_when_vector_missing(self): issue = self._make_issue(cvss_v3_score="7.5") metrics = self._get_metrics(issue) self.assertEqual(len(metrics), 1) self.assertNotIn("cvssV3_1", metrics[0]) def test_v4_omitted_when_score_missing(self): vector = "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N" issue = self._make_issue(cvss_v4_vector_string=vector) metrics = self._get_metrics(issue) self.assertEqual(len(metrics), 1) self.assertNotIn("cvssV4_0", metrics[0]) def test_v4_omitted_when_vector_missing(self): issue = self._make_issue(cvss_v4_score="8.7") metrics = self._get_metrics(issue) self.assertEqual(len(metrics), 1) self.assertNotIn("cvssV4_0", metrics[0]) class PreReleaseChecklistTestCase(BaseChecklistTestCaseMixin, TestCase): checklist_class = PreRelease status_to_version = { "a": "alpha", "b": "beta", "rc": "release candidate", } def test_affected_releases(self): feature_release = self.factory.make_feature_release_checklist("6.0") for status, verbose in self.status_to_version.items(): release = self.factory.make_release(version=f"6.0{status}1") with self.subTest(release=release): checklist = self.make_checklist( feature_release=feature_release, release=release ) self.assertEqual(checklist.affected_releases, [release]) def test_blogpost_info(self): feature_release = self.factory.make_feature_release_checklist("6.0") for status, verbose in self.status_to_version.items(): release = self.factory.make_release(version=f"6.0{status}1") with self.subTest(release=release): checklist = self.make_checklist( feature_release=feature_release, release=release ) self.assertEqual( checklist.blogpost_title, f"Django 6.0 {verbose} 1 released" ) self.assertEqual( checklist.blogpost_template, f"checklists/release_{checklist.status_reversed}_blogpost.md", ) expected = ( f"Today Django 6.0 {verbose} 1, a preview/testing package for the " f"upcoming Django 6.0 release, is available." ) self.assertEqual(checklist.blogpost_summary, expected) def test_versions(self): feature_release = self.factory.make_feature_release_checklist("6.0") for status, verbose in self.status_to_version.items(): version = f"6.0{status}1" release = self.factory.make_release(version=version) with self.subTest(release=release): checklist = self.make_checklist( feature_release=feature_release, release=release ) self.assertEqual(checklist.version, version) self.assertEqual(checklist.versions, [version]) def test_render_checklist(self): feature_release = self.factory.make_feature_release_checklist("5.2") for status, version in self.status_to_version.items(): release = self.factory.make_release( version=f"5.2{status}1", date=date(2025, 4, 2), is_lts=True ) with self.subTest(version=version): instance = self.make_checklist( feature_release=feature_release, release=release ) assert instance.release is release checklist_content = self.do_render_checklist(instance) self.assertIn( "- [ ] Update the translation catalogs:", checklist_content ) if status == "rc": self.assertIn( "- [ ] Create a new topic in the `Internationalization` " "category in the Discourse forum", checklist_content, ) else: self.assertNotInChecklistContent( "- [ ] Post on Forum calling for translations!", checklist_content, ) final_version_correct = f"Django 5.2 {version} 1 is now available." self.assertIn(final_version_correct, checklist_content) class FeatureReleaseChecklistTestCase(BaseChecklistTestCaseMixin, TestCase): checklist_class = FeatureRelease def test_affected_releases(self): release = self.factory.make_release(version="6.0") checklist = self.make_checklist(release=release) self.assertEqual(checklist.affected_releases, [release]) def test_blogpost_info(self): release = self.factory.make_release(version="6.0") checklist = self.make_checklist(release=release) self.assertEqual(checklist.blogpost_title, "Django 6.0 released") self.assertEqual( checklist.blogpost_template, "checklists/release_final_blogpost.md" ) self.assertEqual(checklist.blogpost_summary, "Django 6.0 has been released!") def test_versions(self): release = self.factory.make_release(version="6.0") checklist = self.make_checklist(release=release) self.assertEqual(checklist.version, "6.0") self.assertEqual(checklist.versions, ["6.0"]) def test_render_checklist(self): eol_release = self.factory.make_release(version="5.0", date=date(2023, 12, 4)) eom_release = self.factory.make_release(version="5.1", date=date(2024, 9, 2)) release = self.factory.make_release(version="5.2", date=date(2025, 4, 2)) checklist = self.make_checklist( release=release, eom_release=eom_release, eol_release=eol_release ) checklist_content = self.do_render_checklist(checklist) version_trove_classifier_updates = ( "- [ ] Change version in `django/__init__.py` and maybe trove classifier:\n" ' - `VERSION = (5, 2, 0, "final", 0)`\n' ' - Ensure the "Development Status" trove classifier in ' "`pyproject.toml` is: `Development Status :: 5 - Production/Stable`\n" " - `git commit -a -m '[5.2.x] Bumped version for 5.2 release.'`\n" ) post_release_bump = ( "- [ ] BUMP **MINOR VERSION** in `django/__init__.py`\n" ' - `VERSION = (5, 2, 1, "alpha", 0)`\n' " - `git commit -a -m '[5.2.x] Post-release version bump.'`" ) feature_release_tasks = [ "- Remove the `UNDER DEVELOPMENT` header at the top of the release notes", "- Remove the `Expected` prefix and update the release date if necessary", "- [ ] Create a new branch from the current stable branch in the " "[django-docs-translations repository]", "- [ ] Update the metadata for the docs in " "https://www.djangoproject.com/admin/docs/documentrelease/", "- Create new `DocumentRelease` objects for each language", "- [ ] Extend [robots.docs.txt](https://github.com/django/" "djangoproject.com/blob/main/djangoproject/static/robots.docs.txt)", "- [ ] Advance the version in the download page's tables", "- [ ] Update the current stable branch and remove the pre-release branch", version_trove_classifier_updates, post_release_bump, ] for feature_release_task in feature_release_tasks: with self.subTest(task=feature_release_task): self.assertIn(feature_release_task, checklist_content) with self.subTest(task="Stub release notes added"): self.assertStubReleaseNotesAdded(release, checklist_content) with self.subTest(task="Make release public steps added"): self.assertMakeReleasePublicAdded(release, checklist_content) with self.subTest(task="Push and announce steps added"): self.assertPushAndAnnouncesAdded(checklist, checklist_content) with self.subTest(taks="Blogpost EOM and EOL Release"): self.assertIn( "With the release of Django 5.2, Django 5.1\nhas reached the " "end of mainstream support.", checklist_content, ) self.assertIn( "Django 5.0 has reached the end of extended support.", checklist_content ) class NaturalKeyTestCase(TestCase): factory = Factory() def test_releaser_natural_key(self): user = self.factory.make_user(username="testuser") releaser = self.factory.make_releaser(user=user) natural_key = releaser.natural_key() self.assertEqual(natural_key, ("testuser",)) retrieved = Releaser.objects.get_by_natural_key("testuser") self.assertEqual(retrieved, releaser) def test_feature_release_natural_key(self): release = self.factory.make_release(version="6.0") checklist = self.factory.make_checklist(FeatureRelease, release=release) natural_key = checklist.natural_key() self.assertEqual(natural_key, ("6.0",)) retrieved = FeatureRelease.objects.get_by_natural_key("6.0") self.assertEqual(retrieved, checklist) def test_pre_release_natural_key(self): feature_release = self.factory.make_feature_release_checklist("6.0") release = self.factory.make_release(version="6.0a1") checklist = self.factory.make_checklist( PreRelease, feature_release=feature_release, release=release ) natural_key = checklist.natural_key() self.assertEqual(natural_key, ("6.0a1",)) retrieved = PreRelease.objects.get_by_natural_key("6.0a1") self.assertEqual(retrieved, checklist) def test_bugfix_release_natural_key(self): release = self.factory.make_release(version="5.2.4") checklist = self.factory.make_checklist(BugFixRelease, release=release) natural_key = checklist.natural_key() self.assertEqual(natural_key, ("5.2.4",)) retrieved = BugFixRelease.objects.get_by_natural_key("5.2.4") self.assertEqual(retrieved, checklist) def test_security_release_natural_key(self): when = make_aware(datetime(2025, 4, 2, 14, 30)) checklist = self.factory.make_security_checklist(releases=[], when=when) natural_key = checklist.natural_key() self.assertEqual(natural_key, (when.isoformat(),)) retrieved = SecurityRelease.objects.get_by_natural_key(when.isoformat()) self.assertEqual(retrieved, checklist) def test_security_issue_natural_key(self): checklist = self.factory.make_security_checklist(releases=[]) issue = self.factory.make_security_issue( checklist, cve_year_number="CVE-2025-12345" ) natural_key = issue.natural_key() self.assertEqual(natural_key, ("CVE-2025-12345",)) retrieved = SecurityIssue.objects.get_by_natural_key("CVE-2025-12345") self.assertEqual(retrieved, issue) def test_security_issue_releases_through_natural_key(self): release = self.factory.make_release(version="5.2") checklist = self.factory.make_security_checklist(releases=[]) issue = self.factory.make_security_issue( checklist, [release], cve_year_number="CVE-2025-54321" ) through = SecurityIssueReleasesThrough.objects.get( securityissue=issue, release=release ) natural_key = through.natural_key() self.assertEqual(natural_key, ("CVE-2025-54321", "5.2")) retrieved = SecurityIssueReleasesThrough.objects.get_by_natural_key( "CVE-2025-54321", "5.2" ) self.assertEqual(retrieved, through)