← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~lgp171188/launchpad:vulnerability-set-view into launchpad:master

 

Guruprasad has proposed merging ~lgp171188/launchpad:vulnerability-set-view into launchpad:master.

Commit message:
Add a distribution vulnerability listing pag

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~lgp171188/launchpad/+git/launchpad/+merge/430357
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~lgp171188/launchpad:vulnerability-set-view into launchpad:master.
diff --git a/lib/lp/bugs/browser/configure.zcml b/lib/lp/bugs/browser/configure.zcml
index de5d592..e5d11ae 100644
--- a/lib/lp/bugs/browser/configure.zcml
+++ b/lib/lp/bugs/browser/configure.zcml
@@ -907,6 +907,17 @@
         class="lp.bugs.browser.vulnerability.VulnerabilityIndexView"
         permission="zope.Public"
         template="../templates/vulnerability-index.pt" />
+    <browser:page
+        name="+listing-detailed"
+        for="lp.bugs.interfaces.vulnerability.IVulnerability"
+        permission="zope.Public"
+        template="../templates/vulnerability-listing-detailed.pt"/>
+    <browser:page
+        name="+vulnerabilities"
+        for="lp.registry.interfaces.distribution.IDistribution"
+        class="lp.bugs.browser.vulnerability.VulnerabilitySetIndexView"
+        permission="zope.Public"
+        template="../templates/vulnerabilityset-index.pt" />
     <browser:url
         for="lp.bugs.interfaces.bugsubscription.IBugSubscription"
         path_expression="string:+subscription/${person/name}"
diff --git a/lib/lp/bugs/browser/tests/test_vulnerability.py b/lib/lp/bugs/browser/tests/test_vulnerability.py
index 1a0fcc2..40815d1 100644
--- a/lib/lp/bugs/browser/tests/test_vulnerability.py
+++ b/lib/lp/bugs/browser/tests/test_vulnerability.py
@@ -2,14 +2,23 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Vulnerability browser tests"""
+import random
 from datetime import datetime, timezone
 
 from soupmatchers import HTMLContains, Tag, Within
 from testtools.matchers import MatchesAll, Not
 
 from lp.services.webapp import canonical_url
-from lp.testing import ANONYMOUS, BrowserTestCase, login, person_logged_in
+from lp.testing import (
+    ANONYMOUS,
+    BrowserTestCase,
+    login,
+    person_logged_in,
+    record_two_runs,
+)
 from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.matchers import HasQueryCount
+from lp.testing.views import create_initialized_view
 
 
 class TestVulnerabilityPage(BrowserTestCase):
@@ -251,3 +260,203 @@ class TestVulnerabilityPage(BrowserTestCase):
                 ),
             ),
         )
+
+
+class TestVulnerabilityListingPage(BrowserTestCase):
+    """Tests for the vulnerability listing page."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super().setUp()
+        self.cve_index = 1
+
+    def makeVulnerabilitiesAndMatchers(
+        self, distribution, count, cve_sequence_start=1234
+    ):
+        vulnerability_matchers = []
+        for _ in range(count):
+            cve = self.factory.makeCVE("2022-%04i" % self.cve_index)
+            self.cve_index += 1
+            vulnerability_matchers.append(
+                Tag(
+                    "vulnerability link",
+                    "a",
+                    attrs={
+                        "href": canonical_url(
+                            self.factory.makeVulnerability(
+                                distribution=distribution, cve=cve
+                            )
+                        )
+                    },
+                )
+            )
+        return vulnerability_matchers
+
+    def assertBatches(self, context, link_matchers, batched, start, size):
+        view = create_initialized_view(context, "+vulnerabilities")
+        listing_tag = Tag(
+            "vulnerability listing", "div", attrs={"class": "listing"}
+        )
+        batch_nav_tag = Tag(
+            "batch nav links", "td", attrs={"class": "batch-navigation-links"}
+        )
+        present_links = ([batch_nav_tag] if batched else []) + [
+            matcher
+            for i, matcher in enumerate(link_matchers)
+            if i in range(start, start + size)
+        ]
+        absent_links = ([] if batched else [batch_nav_tag]) + [
+            matcher
+            for i, matcher in enumerate(link_matchers)
+            if i not in range(start, start + size)
+        ]
+        self.assertThat(
+            view.render(),
+            MatchesAll(
+                HTMLContains(listing_tag, *present_links),
+                Not(HTMLContains(*absent_links)),
+            ),
+        )
+
+    def assertVulnerabilitiesQueryCount(self, context, create_func):
+        self.pushConfig("launchpad", default_batch_size=5)
+        recorder1, recorder2 = record_two_runs(
+            lambda: self.getMainText(context, "+vulnerabilities"),
+            create_func,
+            5,
+        )
+        # Even though there are 10 vulnerabilities and CVEs in the 2nd run,
+        # only the CVEs related to the first 5 (batch size) vulnerabilities,
+        # if present, are queried due to the batching. If some vulnerabilities
+        # do not have a linked CVE, the query count will reduce by the total
+        # number of such vulnerabilities.
+        self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
+
+    def test_vulnerabilities_with_cves_listing_query_count(self):
+        distribution = self.factory.makeDistribution()
+        cve_sequences = set()
+
+        def create_vulnerability():
+            while True:
+                cve_sequence = random.randint(1000, 9999)
+                if cve_sequence not in cve_sequences:
+                    cve_sequences.add(cve_sequence)
+                    login(ANONYMOUS)
+                    cve = self.factory.makeCVE(
+                        sequence="2022-{}".format(cve_sequence)
+                    )
+                    self.factory.makeVulnerability(
+                        distribution=distribution, cve=cve
+                    )
+                    break
+
+        self.assertVulnerabilitiesQueryCount(
+            distribution, create_vulnerability
+        )
+
+    def test_vulnerabilities_without_cve_listing_query_count(self):
+        distribution = self.factory.makeDistribution()
+
+        def create_vulnerability():
+            login(ANONYMOUS)
+            self.factory.makeVulnerability(distribution=distribution)
+
+        self.assertVulnerabilitiesQueryCount(
+            distribution, create_vulnerability
+        )
+
+    def test_vulnerability_listing(self):
+        distribution = self.factory.makeDistribution()
+        vulnerability1 = self.factory.makeVulnerability(
+            distribution=distribution
+        )
+        vulnerability2 = self.factory.makeVulnerability()
+        cve = self.factory.makeCVE(sequence="2022-1234")
+        vulnerability3 = self.factory.makeVulnerability(
+            distribution=distribution, cve=cve
+        )
+        browser = self.getUserBrowser(
+            canonical_url(distribution) + "/+vulnerabilities",
+            user=self.factory.makePerson(),
+        )
+        login(ANONYMOUS)
+        self.assertThat(
+            browser.contents,
+            Not(
+                HTMLContains(
+                    Tag(
+                        "vulnerability 2 link",
+                        "a",
+                        attrs={
+                            "href": canonical_url(vulnerability2),
+                        },
+                    )
+                )
+            ),
+        )
+        for vulnerability in (vulnerability1, vulnerability3):
+            vulnerability_div = Tag(
+                "vulnerability div",
+                "div",
+                attrs={
+                    "id": "vulnerability-{}".format(vulnerability.id),
+                },
+            )
+            vulnerability_img = Tag(
+                "vulnerability img",
+                "img",
+                attrs={
+                    "src": "/@@/cve",
+                },
+            )
+            vulnerability_link = Tag(
+                "vulnerability link",
+                "a",
+                attrs={
+                    "href": canonical_url(vulnerability),
+                },
+            )
+
+            description = ""
+            if vulnerability.description:
+                description = vulnerability.description
+            elif vulnerability.cve:
+                description = vulnerability.cve.description
+
+            if not description:
+                vulnerability_description = Tag(
+                    "vulnerability description", "div"
+                )
+            else:
+                vulnerability_description = Tag(
+                    "vulnerability description", "div", text=description
+                )
+            self.assertThat(
+                browser.contents,
+                HTMLContains(
+                    Within(
+                        vulnerability_div,
+                        vulnerability_img,
+                    ),
+                    Within(
+                        vulnerability_div,
+                        vulnerability_link,
+                    ),
+                    Within(
+                        vulnerability_div,
+                        vulnerability_description,
+                    ),
+                ),
+            )
+
+    def test_vulnerability_batched_listing(self):
+        distribution = self.factory.makeDistribution()
+        link_matchers = self.makeVulnerabilitiesAndMatchers(distribution, 3)
+        self.assertBatches(distribution, link_matchers, False, 0, 3)
+        link_matchers.extend(
+            self.makeVulnerabilitiesAndMatchers(
+                distribution, 7, cve_sequence_start=2345
+            )
+        )
+        self.assertBatches(distribution, link_matchers, True, 0, 5)
diff --git a/lib/lp/bugs/browser/vulnerability.py b/lib/lp/bugs/browser/vulnerability.py
index 3d40cc1..2071463 100644
--- a/lib/lp/bugs/browser/vulnerability.py
+++ b/lib/lp/bugs/browser/vulnerability.py
@@ -5,9 +5,12 @@
 
 __all__ = [
     "VulnerabilityIndexView",
+    "VulnerabilitySetIndexView",
 ]
 
 from lp.bugs.browser.buglinktarget import BugLinksListingView
+from lp.services.webapp import LaunchpadView
+from lp.services.webapp.batching import BatchNavigator
 from lp.services.webapp.breadcrumb import TitleBreadcrumb
 
 
@@ -28,3 +31,16 @@ class VulnerabilityIndexView(BugLinksListingView):
         return "{} in the {} distribution".format(
             displayname, self.context.distribution.displayname
         )
+
+
+class VulnerabilitySetIndexView(LaunchpadView):
+    @property
+    def label(self):
+        return "{} vulnerabilities".format(self.context.displayname)
+
+    @property
+    def page_title(self):
+        return self.label
+
+    def getAllBatched(self):
+        return BatchNavigator(self.context.vulnerabilities, self.request)
diff --git a/lib/lp/bugs/templates/vulnerability-listing-detailed.pt b/lib/lp/bugs/templates/vulnerability-listing-detailed.pt
new file mode 100644
index 0000000..315587f
--- /dev/null
+++ b/lib/lp/bugs/templates/vulnerability-listing-detailed.pt
@@ -0,0 +1,23 @@
+<tal:root
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  omit-tag="">
+  <div tal:attributes="id string:vulnerability-${context/id}">
+    <img src="/@@/cve" />
+    <a tal:attributes="href context/fmt:url"
+       tal:content="context/title"></a>
+    <div style="margin-left: 25px"
+        tal:condition="context/description"
+        tal:content="context/description">
+    </div>
+    <div style="margin-left: 25px"
+        tal:condition="python: not context.description and context.cve"
+        tal:content="context/cve/description">
+    </div>
+    <div style="margin-left: 25px"
+        tal:condition="python: not context.description and not context.cve"
+        tal:content="context/description">
+    </div>
+  </div>
+  <hr />
+</tal:root>
diff --git a/lib/lp/bugs/templates/vulnerabilityset-index.pt b/lib/lp/bugs/templates/vulnerabilityset-index.pt
new file mode 100644
index 0000000..ab05b65
--- /dev/null
+++ b/lib/lp/bugs/templates/vulnerabilityset-index.pt
@@ -0,0 +1,25 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml";
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  xml:lang="en"
+  lang="en"
+  dir="ltr"
+  metal:use-macro="view/macro:page/main_only"
+  i18n:domain="malone"
+>
+  <body>
+    <div metal:fill-slot="main"
+         tal:define="batchnav view/getAllBatched;
+                     batch batchnav/currentBatch">
+      <tal:navigation
+          tal:condition="batchnav/has_multiple_pages"
+          replace="structure batchnav/@@+navigation-links-upper" />
+      <div class="listing">
+        <div tal:repeat="vulnerability batch"
+            tal:replace="structure vulnerability/@@+listing-detailed" />
+      </div>
+    </div>
+  </body>
+</html>

Follow ups