diff --git a/extensions/package-vulnerability-scanner/manifest.json b/extensions/package-vulnerability-scanner/manifest.json index 0776c48f..f6fe8014 100644 --- a/extensions/package-vulnerability-scanner/manifest.json +++ b/extensions/package-vulnerability-scanner/manifest.json @@ -23,11 +23,11 @@ "dist/assets/index-CKUnK_gI.css": { "checksum": "910900dfbf81f7ac4caf125b844a495c" }, - "dist/assets/index-BR_SAMDl.js": { - "checksum": "1641c59b4d18d7029985a3b98890a397" + "dist/assets/index-PKuzyQYK.js": { + "checksum": "bd612838b6ccf0c3a03241de2048eec3" }, "dist/index.html": { - "checksum": "5bb81dbee09e839af32e17caedc8a436" + "checksum": "bdc6e932f6c401d4db24a1157178352a" }, "main.py": { "checksum": "ea913b00aaac84531ee46021969f4eaa" diff --git a/extensions/package-vulnerability-scanner/src/components/ContentCard.test.ts b/extensions/package-vulnerability-scanner/src/components/ContentCard.test.ts new file mode 100644 index 00000000..5d4f0d65 --- /dev/null +++ b/extensions/package-vulnerability-scanner/src/components/ContentCard.test.ts @@ -0,0 +1,71 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach } from "vitest"; +import { setActivePinia, createPinia } from "pinia"; +import { mount } from "@vue/test-utils"; + +import ContentCard from "./ContentCard.vue"; +import type { Content } from "../stores/scanner"; +import { useVulnsStore } from "../stores/vulns"; + +function makeContent(overrides: Partial = {}): Content { + return { + guid: "g1", + title: "My Report", + bundle_id: "b1", + packages: [], + vulnerabilityCount: 0, + isLoadingPackages: false, + ...overrides, + } as Content; +} + +describe("ContentCard", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it("shows a Not scanned badge when the scan has failed", () => { + useVulnsStore().error = new Error( + "Couldn't fetch vulnerabilities from Package Manager.", + ); + + const wrapper = mount(ContentCard, { + props: { content: makeContent() }, + }); + + expect(wrapper.text()).toContain("Not scanned"); + expect(wrapper.text()).not.toContain("No vulnerabilities"); + }); + + it("shows a Loading badge while the scan is in progress", () => { + // A fresh vulns store has not fetched yet, so the scan is still in progress. + const wrapper = mount(ContentCard, { + props: { content: makeContent() }, + }); + + expect(wrapper.text()).toContain("Loading..."); + }); + + it("shows the vulnerability count once the scan has completed", () => { + useVulnsStore().isFetched = true; // scan no longer in progress + + const wrapper = mount(ContentCard, { + props: { content: makeContent({ vulnerabilityCount: 2 }) }, + }); + + expect(wrapper.text()).toContain("2 vulnerabilities"); + }); + + it("hides the scan badge entirely when this item's own packages failed to load", () => { + useVulnsStore().error = new Error("vuln service down"); + + const wrapper = mount(ContentCard, { + props: { + content: makeContent({ packageFetchError: new Error("boom") }), + }, + }); + + expect(wrapper.text()).not.toContain("Not scanned"); + expect(wrapper.text()).not.toContain("Loading..."); + }); +}); diff --git a/extensions/package-vulnerability-scanner/src/components/ContentCard.vue b/extensions/package-vulnerability-scanner/src/components/ContentCard.vue index 8877d78b..58910ba1 100644 --- a/extensions/package-vulnerability-scanner/src/components/ContentCard.vue +++ b/extensions/package-vulnerability-scanner/src/components/ContentCard.vue @@ -54,6 +54,10 @@ function handleClick() { Loading... + + Not scanned + + {{ vulnerabilityText }} @@ -88,7 +92,7 @@ function handleClick() { Incomplete content - Error loading packages + Couldn't load packages Loading packages... diff --git a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts new file mode 100644 index 00000000..be5e3d7b --- /dev/null +++ b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.test.ts @@ -0,0 +1,79 @@ +// @vitest-environment happy-dom +import { describe, it, expect, beforeEach } from "vitest"; +import { setActivePinia, createPinia } from "pinia"; +import { mount } from "@vue/test-utils"; + +import VulnerabilityChecker from "./VulnerabilityChecker.vue"; +import type { Content } from "../stores/scanner"; +import { useVulnsStore } from "../stores/vulns"; +import { usePackagesStore } from "../stores/packages"; + +function makeContent(overrides: Partial = {}): Content { + return { + guid: "g1", + title: "My Report", + bundle_id: "b1", + packages: [], + vulnerabilityCount: 0, + isLoadingPackages: false, + ...overrides, + } as Content; +} + +describe("VulnerabilityChecker", () => { + beforeEach(() => { + setActivePinia(createPinia()); + }); + + it("shows the specific reason from Connect when packages can't be read", () => { + const content = makeContent({ + packageFetchError: new Error("Connect API error: Content not found"), + }); + + const wrapper = mount(VulnerabilityChecker, { props: { content } }); + + expect(wrapper.text()).toContain("Couldn't load this content's packages"); + expect(wrapper.text()).toContain("Connect API error: Content not found"); + }); + + it("shows the scan failure reason when the vuln lookup itself fails", () => { + // Unlike packageFetchError (this item's own packages), scanFailed reflects a + // store-wide failure (e.g. the vulnerability service), so it applies even + // though this item's packages loaded fine. + usePackagesStore().error = null; + useVulnsStore().error = new Error( + "Couldn't fetch vulnerabilities from Package Manager.", + ); + + const wrapper = mount(VulnerabilityChecker, { + props: { content: makeContent() }, + }); + + expect(wrapper.text()).toContain("Scan couldn't complete"); + expect(wrapper.text()).toContain( + "Couldn't fetch vulnerabilities from Package Manager.", + ); + }); + + it("shows a loading state, not a verdict, while the vuln lookup is pending", () => { + // A fresh vulns store has not fetched yet, so the scan is still in progress + // even though this item's packages have loaded. + const wrapper = mount(VulnerabilityChecker, { + props: { content: makeContent() }, + }); + + expect(wrapper.text()).toContain("Checking for vulnerabilities"); + expect(wrapper.text()).not.toContain("No vulnerabilities found"); + }); + + it("shows the result once the scan has completed", () => { + useVulnsStore().isFetched = true; // scan no longer in progress + + const wrapper = mount(VulnerabilityChecker, { + props: { content: makeContent() }, + }); + + expect(wrapper.text()).not.toContain("Checking for vulnerabilities"); + expect(wrapper.text()).toContain("No vulnerabilities found"); + }); +}); diff --git a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue index d952eab4..0d048ad2 100644 --- a/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue +++ b/extensions/package-vulnerability-scanner/src/components/VulnerabilityChecker.vue @@ -162,7 +162,7 @@ const hasLanguageVersion = computed(() => { /> @@ -170,7 +170,23 @@ const hasLanguageVersion = computed(() => { + + + + +