Skip to content

Commit 022ec38

Browse files
committed
feat(heureka): adds tests for active/remediated vulnerability split logic
1 parent cf561b2 commit 022ec38

2 files changed

Lines changed: 361 additions & 27 deletions

File tree

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React, { Suspense } from "react"
7+
import { act, render, screen } from "@testing-library/react"
8+
import { ObservableQuery } from "@apollo/client"
9+
import { createMemoryHistory, createRootRoute, createRoute, Outlet, RouterProvider } from "@tanstack/react-router"
10+
import { IssuesDataRows } from "./index"
11+
import { GetImagesQuery, GetRemediationsQuery } from "../../../../../generated/graphql"
12+
import { getTestRouter } from "../../../../../mocks/getTestRouter"
13+
14+
// --------------------------------------------------------------------------
15+
// Helpers
16+
// --------------------------------------------------------------------------
17+
18+
/**
19+
* Builds an images promise containing a single image with the given vulnerabilities.
20+
* Each vulnerability is identified by its CVE name (e.g. "CVE-2024-1234").
21+
*/
22+
function makeImagesPromise(cveNames: string[]): Promise<ObservableQuery.Result<GetImagesQuery>> {
23+
return Promise.resolve({
24+
data: {
25+
Images: {
26+
edges: [
27+
{
28+
node: {
29+
id: "img-1",
30+
repository: "repo/image",
31+
imageRegistryUrl: "https://registry.example.com/repo/image",
32+
vulnerabilityCounts: {
33+
critical: 0,
34+
high: cveNames.length,
35+
medium: 0,
36+
low: 0,
37+
none: 0,
38+
total: cveNames.length,
39+
__typename: "SeverityCounts",
40+
},
41+
vulnerabilities: {
42+
edges: cveNames.map((name, i) => ({
43+
node: {
44+
id: `vul-${i}`,
45+
name,
46+
severity: "High",
47+
earliestTargetRemediationDate: "2025-12-31T00:00:00Z",
48+
description: `Description for ${name}`,
49+
sourceUrl: `https://nvd.nist.gov/vuln/detail/${name}`,
50+
__typename: "Vulnerability",
51+
},
52+
__typename: "VulnerabilityEdge",
53+
})),
54+
pageInfo: { pageNumber: 1, pages: [], __typename: "PageInfo" },
55+
__typename: "VulnerabilityConnection",
56+
},
57+
versions: { edges: [], __typename: "ComponentVersionConnection" },
58+
__typename: "Image",
59+
},
60+
__typename: "ImageEdge",
61+
},
62+
],
63+
pageInfo: { pageNumber: 1, pages: [], __typename: "PageInfo" },
64+
__typename: "ImageConnection",
65+
},
66+
},
67+
loading: false,
68+
networkStatus: 7,
69+
error: undefined,
70+
partial: false,
71+
dataState: "complete" as const,
72+
}) as unknown as Promise<ObservableQuery.Result<GetImagesQuery>>
73+
}
74+
75+
/**
76+
* Builds a remediations promise for the given CVE names.
77+
* An empty array means no remediations exist.
78+
*/
79+
function makeRemediationsPromise(remediatedCves: string[]): Promise<ObservableQuery.Result<GetRemediationsQuery>> {
80+
return Promise.resolve({
81+
data: {
82+
Remediations: {
83+
edges: remediatedCves.map((cve, i) => ({
84+
node: {
85+
id: `rem-${i}`,
86+
vulnerability: cve,
87+
__typename: "Remediation",
88+
},
89+
__typename: "RemediationEdge",
90+
})),
91+
pageInfo: { pageNumber: 1, pages: [], __typename: "PageInfo" },
92+
__typename: "RemediationConnection",
93+
},
94+
},
95+
loading: false,
96+
networkStatus: 7,
97+
error: undefined,
98+
partial: false,
99+
dataState: "complete" as const,
100+
}) as unknown as Promise<ObservableQuery.Result<GetRemediationsQuery>>
101+
}
102+
103+
/**
104+
* Wraps the component in a RouterProvider so that IssuesDataRow's useRouteContext works.
105+
*/
106+
function renderWithRouter(
107+
issuesPromise: Promise<ObservableQuery.Result<GetImagesQuery>>,
108+
remediationsPromise: Promise<ObservableQuery.Result<GetRemediationsQuery>>
109+
) {
110+
const rootRoute = createRootRoute({ component: () => <Outlet /> })
111+
const testRoute = createRoute({
112+
getParentRoute: () => rootRoute,
113+
path: "/services/$service",
114+
component: () => (
115+
<Suspense fallback={<div>Loading...</div>}>
116+
<IssuesDataRows
117+
issuesPromise={issuesPromise}
118+
remediationsPromise={remediationsPromise}
119+
service="my-service"
120+
image="repo/image"
121+
onFalsePositiveSuccess={() => {}}
122+
/>
123+
</Suspense>
124+
),
125+
})
126+
const routeTree = rootRoute.addChildren([testRoute])
127+
const router = getTestRouter({
128+
routeTree,
129+
history: createMemoryHistory({ initialEntries: ["/services/my-service"] }),
130+
})
131+
return render(<RouterProvider router={router} />)
132+
}
133+
134+
// --------------------------------------------------------------------------
135+
// Tests
136+
// --------------------------------------------------------------------------
137+
138+
describe("IssuesDataRows — active/remediated split", () => {
139+
it("shows a vulnerability when it has no remediation record (active state)", async () => {
140+
const issuesPromise = makeImagesPromise(["CVE-2024-1234"])
141+
const remediationsPromise = makeRemediationsPromise([]) // no remediations
142+
143+
await act(async () => {
144+
renderWithRouter(issuesPromise, remediationsPromise)
145+
})
146+
147+
expect(await screen.findByText("CVE-2024-1234")).toBeInTheDocument()
148+
})
149+
150+
it("hides a vulnerability that has been marked as false positive (remediation record exists)", async () => {
151+
const issuesPromise = makeImagesPromise(["CVE-2024-1234"])
152+
const remediationsPromise = makeRemediationsPromise(["CVE-2024-1234"]) // this CVE is now remediated
153+
154+
await act(async () => {
155+
render(
156+
<Suspense fallback={<div>Loading...</div>}>
157+
<IssuesDataRows
158+
issuesPromise={issuesPromise}
159+
remediationsPromise={remediationsPromise}
160+
service="my-service"
161+
image="repo/image"
162+
onFalsePositiveSuccess={() => {}}
163+
/>
164+
</Suspense>
165+
)
166+
})
167+
168+
// CVE is in remediations → must NOT appear in the Active tab
169+
expect(screen.queryByText("CVE-2024-1234")).not.toBeInTheDocument()
170+
// And the empty state message should be shown
171+
expect(await screen.findByText("No vulnerabilities found! 🚀")).toBeInTheDocument()
172+
})
173+
174+
it("only hides the remediated CVE when multiple vulnerabilities are present", async () => {
175+
const issuesPromise = makeImagesPromise(["CVE-2024-1234", "CVE-2024-5678"])
176+
const remediationsPromise = makeRemediationsPromise(["CVE-2024-1234"]) // only first one is remediated
177+
178+
await act(async () => {
179+
renderWithRouter(issuesPromise, remediationsPromise)
180+
})
181+
182+
// Remediated CVE must NOT appear in Active tab
183+
expect(screen.queryByText("CVE-2024-1234")).not.toBeInTheDocument()
184+
// Non-remediated CVE must still appear in Active tab
185+
expect(await screen.findByText("CVE-2024-5678")).toBeInTheDocument()
186+
})
187+
188+
it("shows empty state when all vulnerabilities have remediation records", async () => {
189+
const cves = ["CVE-2024-1234", "CVE-2024-5678"]
190+
const issuesPromise = makeImagesPromise(cves)
191+
const remediationsPromise = makeRemediationsPromise(cves) // both are remediated
192+
193+
await act(async () => {
194+
render(
195+
<Suspense fallback={<div>Loading...</div>}>
196+
<IssuesDataRows
197+
issuesPromise={issuesPromise}
198+
remediationsPromise={remediationsPromise}
199+
service="my-service"
200+
image="repo/image"
201+
onFalsePositiveSuccess={() => {}}
202+
/>
203+
</Suspense>
204+
)
205+
})
206+
207+
expect(await screen.findByText("No vulnerabilities found! 🚀")).toBeInTheDocument()
208+
})
209+
})

0 commit comments

Comments
 (0)