Skip to content

Commit

Permalink
feat: add error handling
Browse files Browse the repository at this point in the history
Closes #352
  • Loading branch information
d-koppenhagen committed Dec 22, 2024
1 parent 2ad9854 commit c8a3fb3
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 54 deletions.
20 changes: 5 additions & 15 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,18 @@ on:
branches-ignore:
- main
- gh-pages

jobs:
build-and-test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/[email protected]
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
- name: Build and Run End2End tests with Cypress
uses: cypress-io/github-action@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30
build: npm run build
start: npm run dev
wait-on: "http://localhost:8080"
6 changes: 0 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,3 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
15 changes: 15 additions & 0 deletions cypress/e2e/smoke.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
beforeEach(() => {
cy.visit("/")
})

it("should display the title", () => {
cy.get(".v-toolbar__content").contains("Trivy")
})

it("should contain a file input", () => {
cy.get(".v-file-upload input").should("have.attr", "type", "file")
})

it("should display an alert that no report was loaded", () => {
cy.get(".v-alert")
})
15 changes: 12 additions & 3 deletions e2e/home-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ export class HomePage {
)
}

async fetchTestFileFromUrl(url: string) {
async fetchTestFileFromUrl(url: string, waitForResponse = true) {
await this.urlBtn.click()
const fetchButton = this.page.getByRole("button", { name: "Fetch" })
expect(await fetchButton.isDisabled()).toBe(true)
await this.page.getByRole("textbox", { name: "Url" }).fill(url)
const responsePromise = this.page.waitForResponse(url)
let responsePromise
if (waitForResponse) {
responsePromise = this.page.waitForResponse(url)
}
await fetchButton.click()
await responsePromise
if (responsePromise) {
await responsePromise
}
}

async uploadLocalTestFile(relativePath: string) {
Expand Down Expand Up @@ -138,6 +143,10 @@ export class HomePage {
expect(await detailsRow.getByRole("link").getAttribute("href")).toEqual(url)
}

async getErrorMessage() {
return await this.page.locator("[role=alert].text-error").textContent()
}

private async checkClipboardContents(expectedContents: string[]) {
const clipboardContent = await this.page.evaluate(() =>
navigator.clipboard.readText(),
Expand Down
76 changes: 74 additions & 2 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { HomePage } from "./home-page"
const filenameSchemaVersion1 = "test-result-v1.json"
const filenameSchemaVersion2 = "test-result-v2.json"
const filenameTestManyResults = "test-many-results.json"
const trivyReportUrl = `https://raw.githubusercontent.com/dbsystel/trivy-vulnerability-explorer/refs/heads/e2e-playwright/public/${filenameSchemaVersion2}`
const cveEntries = ["CVE-2021-3450", "CVE-2021-3449", "CVE-2019-14697"]

test("should have the correct title", async ({ page }) => {
Expand All @@ -17,7 +16,9 @@ test("should have the correct title", async ({ page }) => {
test("fetches from URL", async ({ page }) => {
const homePage = new HomePage(page)
await homePage.goto()
await homePage.fetchTestFileFromUrl(trivyReportUrl)
await homePage.fetchTestFileFromUrl(
`https://raw.githubusercontent.com/dbsystel/trivy-vulnerability-explorer/refs/heads/e2e-playwright/public/${filenameSchemaVersion2}`,
)
await homePage.verifyTableResult(5)
await homePage.verifyTrivyignore(cveEntries)
})
Expand Down Expand Up @@ -115,3 +116,74 @@ test("vulnerability details can be shown", async ({ page }) => {
"https://example.org",
)
})

test.describe.parallel("Error handling", () => {
test("Error message for files with wrong format is shown", async ({
page,
}) => {
const homePage = new HomePage(page)
await homePage.goto()

await homePage.uploadLocalTestFile(
"../public/test-invalid-report-format.json",
)
let message = await homePage.getErrorMessage()
expect(message).toContain("Invalid report format")
expect(message).toContain(
"The cannot be parsed. Please make sure the report is in the correct format.",
)

await homePage.uploadLocalTestFile(
"../public/test-invalid-vulnerability-format.json",
)
message = await homePage.getErrorMessage()
expect(message).toContain("Invalid report format")
expect(message).toContain(
"The cannot be parsed. Please make sure the report is in the correct format.",
)
})

test("Error message for a network error is shown", async ({ page }) => {
const homePage = new HomePage(page)
await homePage.goto()

const url = `http://localhost:8080/my-non-existing-file.json`

await homePage.page.route(url, (route) => route.abort("failed"))
await homePage.fetchTestFileFromUrl(url, false)

const message = await homePage.getErrorMessage()
expect(message).toContain("Error when fetching report")
expect(message).toContain("Failed to fetch")
})

test("Error message for an invalid JSON file is shown", async ({ page }) => {
const homePage = new HomePage(page)
await homePage.goto()

const url = `index.html`

await homePage.page.route(url, (route) => route.abort("failed"))
await homePage.fetchTestFileFromUrl(url, false)

const message = await homePage.getErrorMessage()
expect(message).toContain("Error when fetching report")
expect(message).toContain("is not valid JSON")
})

test("Error message for a non 200 OK status is shown", async ({ page }) => {
const homePage = new HomePage(page)
await homePage.goto()

const url = `http://localhost:8080/my-non-existing-file.json`

await homePage.page.route(url, (route) => route.fulfill({ status: 404 }))
await homePage.fetchTestFileFromUrl(url, false)

const message = await homePage.getErrorMessage()
expect(message).toContain("Response not successful")
expect(message).toContain(
"Did not receive Response status 200 OK. Got 404 Not Found",
)
})
})
8 changes: 8 additions & 0 deletions public/test-invalid-report-format.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"SchemaVersion": 0,
"Data": [
{
"Target": "dummy-image:2.0.0"
}
]
}
14 changes: 14 additions & 0 deletions public/test-invalid-vulnerability-format.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"SchemaVersion": 2,
"ArtifactName": "dummy-image:2.0.0",
"ArtifactType": "container_image",
"Metadata": {},
"Results": [
{
"Target": "dummy-image:2.0.0",
"Class": "",
"Type": "",
"Vulnerabilities": [{}]
}
]
}
94 changes: 79 additions & 15 deletions src/components/DataInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
/>

<ReportUrlFetcher
:onNewReport="onNewReport"
:onNewReport="onNewReportFromUrl"
v-if="reportSource === ReportSource.Url"
:presetUrl="presetUrl"

Check failure on line 29 in src/components/DataInput.vue

View workflow job for this annotation

GitHub Actions / build-and-test

Type 'string | undefined' is not assignable to type 'String'.
/>
Expand All @@ -44,11 +44,28 @@
hide-details
/>
</v-toolbar>

<v-alert
v-if="reportError"
class="mt-5"
color="error"
icon="$error"
variant="outlined"
closable
@click:close="reportError = undefined"
:title="reportError.title"
>
<p>{{ reportError.text }}</p>
<a v-if="reportError.link" :href="reportError.link.href" target="_blank">{{
reportError.link.text
}}</a>
</v-alert>
</template>

<script setup lang="ts">
import { ref, computed, watch, defineProps, defineEmits, onMounted } from "vue"
import type {
ReportError,
Version1OrVersion2,
VulnerabilityReportFile,
VulnerabilityReportTarget,
Expand All @@ -66,16 +83,15 @@ const emit = defineEmits(["inputChanged"])
const selectedTarget = ref("")
const reportSource = ref(ReportSource.File)
const vulnerabilityReport = ref<VulnerabilityReportTarget[]>([])
const reportError = ref<ReportError>()
const handleFileUpload = (files: File[] | File) => {
const file = Array.isArray(files) ? files[0] : files
const reader = new FileReader()
const temp = reader.readAsText(file)
reader.onload = (e) => {
const vulnerabilityTargets = parseFile(e)
if (vulnerabilityTargets) {
handleNewReport(vulnerabilityTargets)
}
handleNewReport(vulnerabilityTargets)
}
}
Expand Down Expand Up @@ -117,17 +133,63 @@ const parseFile = (
}
const extractTargetsFromReport = (parsedReport: Version1OrVersion2) => {
reportError.value = undefined
let vulnerabilityTargets: VulnerabilityReportTarget[]
if (isSchemaVersion2(parsedReport)) {
vulnerabilityTargets = parsedReport.Results
} else {
vulnerabilityTargets = parsedReport
function setReportError() {
reportError.value = {
title: "Invalid report format",
text: `The cannot be parsed. Please make sure the report is in the correct format.`,
link: {
text: "For more information check the 'Version1OrVersion2' interface",
href: "https://github.com/dbsystel/trivy-vulnerability-explorer/blob/main/src/types.ts",
},
}
}
vulnerabilityTargets.forEach((vr) =>
vr.Vulnerabilities?.forEach((v) => (v.Target = vr.Target)),
)
return vulnerabilityTargets
try {
if (isSchemaVersion2(parsedReport)) {
vulnerabilityTargets = parsedReport.Results
} else {
vulnerabilityTargets = parsedReport
}
console.log(vulnerabilityTargets)
// validate report format
if (
!vulnerabilityTargets.every((i) => {
const targetValid =
i.Target && i.Vulnerabilities && Array.isArray(i.Vulnerabilities)
const vulnerabilitiesValid = i.Vulnerabilities?.every((v) => {
return v.PkgName && v.VulnerabilityID && v.Title
})
return targetValid && vulnerabilitiesValid
})
) {
setReportError()
return []
}
vulnerabilityTargets.forEach((vr) =>
vr.Vulnerabilities?.forEach((v) => (v.Target = vr.Target)),
)
return vulnerabilityTargets
} catch (e) {
setReportError()
return []
}
}
const onNewReportFromUrl = (
report: Version1OrVersion2 | { error: ReportError },
) => {
if (report instanceof Object && "error" in report) {
reportError.value = report.error
return
}
return onNewReport(report)
}
const onNewReport = (report: Version1OrVersion2) => {
Expand All @@ -136,9 +198,11 @@ const onNewReport = (report: Version1OrVersion2) => {
}
const handleNewReport = (vulnerabilityTargets: VulnerabilityReportTarget[]) => {
vulnerabilityReport.value.splice(0)
vulnerabilityReport.value.push(...vulnerabilityTargets)
emit("inputChanged", selectedVulnerabilities.value)
if (vulnerabilityTargets.length > 0) {
vulnerabilityReport.value.splice(0)
vulnerabilityReport.value.push(...vulnerabilityTargets)
emit("inputChanged", selectedVulnerabilities.value)
}
}
const isSchemaVersion2 = (
Expand Down
Loading

0 comments on commit c8a3fb3

Please sign in to comment.