diff --git a/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionArtifactDetailsPage.test.tsx b/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionArtifactDetailsPage.test.tsx
new file mode 100644
index 000000000..44db4846d
--- /dev/null
+++ b/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionArtifactDetailsPage.test.tsx
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2024 Harness, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react'
+import copy from 'clipboard-copy'
+import userEvent from '@testing-library/user-event'
+import { getByTestId, getByText, render, waitFor } from '@testing-library/react'
+import { useGetDockerArtifactLayersQuery, useGetDockerArtifactManifestQuery } from '@harnessio/react-har-service-client'
+
+import { getTableColumn } from '@ar/utils/testUtils/utils'
+import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
+
+import VersionDetailsPage from '../../VersionDetailsPage'
+import {
+ mockDockerArtifactLayers,
+ mockDockerArtifactManifest,
+ mockDockerManifestList,
+ mockDockerVersionList,
+ mockDockerVersionSummary
+} from './__mockData__'
+
+const mockHistoryPush = jest.fn()
+// eslint-disable-next-line jest-no-mock
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory: () => ({
+ push: mockHistoryPush
+ })
+}))
+
+jest.mock('clipboard-copy', () => ({
+ __esModule: true,
+ default: jest.fn()
+}))
+
+jest.mock('@harnessio/react-har-service-client', () => ({
+ useGetArtifactVersionSummaryQuery: jest.fn().mockImplementation(() => ({
+ data: { content: mockDockerVersionSummary },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ })),
+ useGetDockerArtifactManifestsQuery: jest.fn().mockImplementation(() => ({
+ data: { content: mockDockerManifestList },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ })),
+ getAllArtifactVersions: jest.fn().mockImplementation(
+ () =>
+ new Promise(success => {
+ success({ content: mockDockerVersionList })
+ })
+ ),
+ useGetDockerArtifactLayersQuery: jest.fn().mockImplementation(() => ({
+ data: { content: mockDockerArtifactLayers },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ })),
+ useGetDockerArtifactManifestQuery: jest.fn().mockImplementation(() => ({
+ data: { content: mockDockerArtifactManifest },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ }))
+}))
+
+describe('Verify Docker Version Artifact Details Tab', () => {
+ beforeAll(() => {
+ jest.clearAllMocks()
+ })
+
+ test('Verify Sub Tabs', async () => {
+ const { container } = render(
+
+
+
+ )
+
+ expect(getByTestId(container, 'layers')).toBeInTheDocument()
+ expect(getByTestId(container, 'manifest')).toBeInTheDocument()
+
+ const manifestTab = getByTestId(container, 'manifest')
+ await userEvent.click(manifestTab)
+ expect(mockHistoryPush).toHaveBeenCalledWith(
+ '/registries/1/artifacts/1/versions/1/artifact_details?digest=sha256%3A144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d&detailsTab=manifest'
+ )
+
+ const layersTab = getByTestId(container, 'layers')
+ await userEvent.click(layersTab)
+ expect(mockHistoryPush).toHaveBeenCalledWith(
+ '/registries/1/artifacts/1/versions/1/artifact_details?digest=sha256%3A144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d&detailsTab=layers'
+ )
+ })
+
+ test('Verify Layers Tab', async () => {
+ const { container } = render(
+
+
+
+ )
+
+ expect(container).toHaveTextContent('versionDetails.artifactDetails.layers.imageLayers')
+
+ const getTableRowColumn = (row: number, col: number) => getTableColumn(row, col) as HTMLElement
+
+ const data = mockDockerArtifactLayers.data.layers?.[0]
+ const row = 1
+ expect(getTableRowColumn(row, 1)).toHaveTextContent(row.toString())
+ expect(getTableRowColumn(row, 2)).toHaveTextContent(data?.command as string)
+ expect(getTableRowColumn(row, 3)).toHaveTextContent(data?.size as string)
+ const copyColumn = getTableRowColumn(row, 4)
+ const copyBtn = copyColumn.querySelector('[data-icon="code-copy"]') as HTMLElement
+ expect(copyBtn).toBeInTheDocument()
+ await userEvent.click(copyBtn)
+ expect(copy).toHaveBeenCalledWith(data?.command)
+ })
+
+ test('should show error message if failed to load layers data', async () => {
+ const refetchFn = jest.fn()
+ ;(useGetDockerArtifactLayersQuery as jest.Mock).mockImplementation(() => ({
+ data: null,
+ error: { message: 'Failed to fetch artifact details' },
+ isLoading: false,
+ refetch: refetchFn
+ }))
+ const { container } = render(
+
+
+
+ )
+ expect(getByText(container, 'Failed to fetch artifact details')).toBeInTheDocument()
+ const retryBtn = container.querySelector('button[aria-label=Retry]')
+ expect(retryBtn).toBeInTheDocument()
+ await userEvent.click(retryBtn!)
+ await waitFor(() => {
+ expect(refetchFn).toHaveBeenCalled()
+ })
+ })
+
+ test('Verify Manifest Tab', async () => {
+ const { container } = render(
+
+
+
+ )
+
+ expect(container).toHaveTextContent('versionDetails.artifactDetails.tabs.manifest')
+
+ const copyBtn = container.querySelector('[data-icon="code-copy"]') as HTMLElement
+ expect(copyBtn).toBeInTheDocument()
+ await userEvent.click(copyBtn)
+ expect(copy).toHaveBeenCalledWith(mockDockerArtifactManifest.data.manifest)
+ })
+
+ test('should show error message if failed to load manifest data', async () => {
+ const refetchFn = jest.fn()
+ ;(useGetDockerArtifactManifestQuery as jest.Mock).mockImplementation(() => ({
+ data: null,
+ error: { message: 'Failed to fetch artifact details' },
+ isLoading: false,
+ refetch: refetchFn
+ }))
+ const { container } = render(
+
+
+
+ )
+ expect(getByText(container, 'Failed to fetch artifact details')).toBeInTheDocument()
+ const retryBtn = container.querySelector('button[aria-label=Retry]')
+ expect(retryBtn).toBeInTheDocument()
+ await userEvent.click(retryBtn!)
+ await waitFor(() => {
+ expect(refetchFn).toHaveBeenCalled()
+ })
+ })
+})
diff --git a/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionHeader.test.tsx b/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionHeader.test.tsx
index 70dbc8b2f..8d397dfd1 100644
--- a/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionHeader.test.tsx
+++ b/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionHeader.test.tsx
@@ -20,6 +20,7 @@ import { getByTestId, render, waitFor } from '@testing-library/react'
import {
type DockerManifestDetails,
getAllArtifactVersions,
+ useGetArtifactVersionSummaryQuery,
useGetDockerArtifactManifestsQuery
} from '@harnessio/react-har-service-client'
@@ -29,7 +30,12 @@ import { testSelectChange } from '@ar/utils/testUtils/utils'
import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
import VersionDetailsPage from '../../VersionDetailsPage'
-import { mockDockerManifestList, mockDockerVersionList, mockDockerVersionSummary } from './__mockData__'
+import {
+ mockDockerManifestList,
+ mockDockerVersionList,
+ mockDockerVersionSummary,
+ mockDockerVersionSummaryWithoutSscaAndStoData
+} from './__mockData__'
const mockHistoryPush = jest.fn()
// eslint-disable-next-line jest-no-mock
@@ -124,7 +130,7 @@ describe('Verify DockerVersionHeader component render', () => {
await testSelectChange(versionSelector, '1.0.1', data.version)
await waitFor(() => {
- expect(mockHistoryPush).toHaveBeenLastCalledWith('/registries/artifacts/versions/1.0.1')
+ expect(mockHistoryPush).toHaveBeenCalledWith('/registries/artifacts/versions/1.0.1')
})
})
@@ -262,4 +268,80 @@ describe('Verify DockerVersionHeader component render', () => {
const selectPopover = dialogs[0] as HTMLElement
expect(selectPopover).toHaveTextContent('No items found')
})
+
+ test('verify tab navigation with ssca and sto data', async () => {
+ const { container } = render(
+
+
+
+ )
+
+ const overviewTab = container.querySelector('div[data-tab-id=overview]')
+ await userEvent.click(overviewTab!)
+ expect(mockHistoryPush).toHaveBeenLastCalledWith(
+ '/registries/undefined/artifacts/undefined/versions/undefined/overview?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+
+ const artifactDetailsTab = container.querySelector('div[data-tab-id=artifact_details]')
+ await userEvent.click(artifactDetailsTab!)
+ expect(mockHistoryPush).toHaveBeenLastCalledWith(
+ '/registries/undefined/artifacts/undefined/versions/undefined/artifact_details?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+
+ const sscaTab = container.querySelector('div[data-tab-id=supply_chain]')
+ await userEvent.click(sscaTab!)
+ expect(mockHistoryPush).toHaveBeenLastCalledWith(
+ '/registries/undefined/artifacts/undefined/versions/undefined/artifact-sources/67a5dccf6d75916b0c3ea1b5/artifacts/67a5dccf6d75916b0c3ea1b6/supply_chain?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+
+ const stoTab = container.querySelector('div[data-tab-id=security_tests]')
+ await userEvent.click(stoTab!)
+ expect(mockHistoryPush).toHaveBeenLastCalledWith(
+ '/registries/undefined/artifacts/undefined/versions/undefined/pipelines/HARNESS_ARTIFACT_SCAN_PIPELINE/executions/Tbi7s6nETjmOMKU3Qrnm7A/security_tests?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+ })
+
+ test('verify tab navigation with ssca and sto data', async () => {
+ ;(useGetArtifactVersionSummaryQuery as jest.Mock).mockImplementation(() => ({
+ data: { content: mockDockerVersionSummaryWithoutSscaAndStoData },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ }))
+ const { container } = render(
+
+
+
+ )
+
+ const overviewTab = container.querySelector('div[data-tab-id=overview]')
+ await userEvent.click(overviewTab!)
+ expect(mockHistoryPush).toHaveBeenLastCalledWith(
+ '/registries/undefined/artifacts/undefined/versions/undefined/overview?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+
+ const artifactDetailsTab = container.querySelector('div[data-tab-id=artifact_details]')
+ await userEvent.click(artifactDetailsTab!)
+ expect(mockHistoryPush).toHaveBeenLastCalledWith(
+ '/registries/undefined/artifacts/undefined/versions/undefined/artifact_details?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+
+ const sscaTab = container.querySelector('div[data-tab-id=supply_chain]')
+ await userEvent.click(sscaTab!)
+ expect(mockHistoryPush).toHaveBeenLastCalledWith(
+ '/registries/undefined/artifacts/undefined/versions/undefined/supply_chain?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+
+ const stoTab = container.querySelector('div[data-tab-id=security_tests]')
+ await userEvent.click(stoTab!)
+ expect(mockHistoryPush).toHaveBeenLastCalledWith(
+ '/registries/undefined/artifacts/undefined/versions/undefined/security_tests?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+ })
})
diff --git a/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionOverviewPage.test.tsx b/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionOverviewPage.test.tsx
index 3d3f11244..12269c10b 100644
--- a/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionOverviewPage.test.tsx
+++ b/web/src/ar/pages/version-details/DockerVersion/__tests__/DockerVersionOverviewPage.test.tsx
@@ -21,6 +21,7 @@ import {
useGetDockerArtifactDetailsQuery,
useGetDockerArtifactIntegrationDetailsQuery
} from '@harnessio/react-har-service-client'
+import { downloadSbom } from '@harnessio/react-ssca-manager-client'
import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
@@ -34,10 +35,31 @@ import {
mockDockerArtifactDetails,
mockDockerArtifactIntegrationDetails,
mockDockerManifestList,
+ mockDockerSbomData,
mockDockerVersionList,
mockDockerVersionSummary
} from './__mockData__'
+const mockHistoryPush = jest.fn()
+// eslint-disable-next-line jest-no-mock
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory: () => ({
+ push: mockHistoryPush
+ })
+}))
+
+const showErrorToast = jest.fn()
+
+jest.mock('@harnessio/uicore', () => ({
+ ...jest.requireActual('@harnessio/uicore'),
+ useToaster: jest.fn().mockImplementation(() => ({
+ showSuccess: jest.fn(),
+ showError: showErrorToast,
+ clear: jest.fn()
+ }))
+}))
+
jest.mock('@harnessio/react-har-service-client', () => ({
useGetArtifactVersionSummaryQuery: jest.fn().mockImplementation(() => ({
data: { content: mockDockerVersionSummary },
@@ -71,6 +93,15 @@ jest.mock('@harnessio/react-har-service-client', () => ({
}))
}))
+jest.mock('@harnessio/react-ssca-manager-client', () => ({
+ downloadSbom: jest.fn().mockImplementation(
+ () =>
+ new Promise(success => {
+ success({ content: mockDockerSbomData })
+ })
+ )
+}))
+
describe('Verify docker version overview page', () => {
beforeAll(() => {
jest.clearAllMocks()
@@ -99,12 +130,24 @@ describe('Verify docker version overview page', () => {
expect(deploymentCard).toHaveTextContent(data.deploymentsDetails?.totalDeployment?.toString() as string)
expect(deploymentCard).toHaveTextContent(data.buildDetails?.pipelineDisplayName as string)
expect(deploymentCard).toHaveTextContent(data.buildDetails?.pipelineExecutionId as string)
+ await userEvent.click(deploymentCard)
+ await waitFor(() => {
+ expect(mockHistoryPush).toHaveBeenCalledWith(
+ '/registries/1/artifacts/1/versions/1/deployments?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+ })
const sscaCard = getByTestId(container, 'integration-supply-chain-card')
expect(sscaCard).toBeInTheDocument()
expect(sscaCard).toHaveTextContent(data.sbomDetails?.componentsCount?.toString() as string)
expect(sscaCard).toHaveTextContent(Number(data.sbomDetails?.avgScore).toFixed(2) as string)
expect(getByText(sscaCard, 'versionDetails.cards.supplyChain.downloadSbom')).toBeInTheDocument()
+ await userEvent.click(sscaCard)
+ await waitFor(() => {
+ expect(mockHistoryPush).toHaveBeenCalledWith(
+ '/registries/1/artifacts/1/versions/1/artifact-sources/67a5dccf6d75916b0c3ea1b5/artifacts/67a5dccf6d75916b0c3ea1b6/supply_chain?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+ })
const securityTestsCard = getByTestId(container, 'integration-security-tests-card')
expect(securityTestsCard).toBeInTheDocument()
@@ -113,6 +156,62 @@ describe('Verify docker version overview page', () => {
expect(securityTestsCard).toHaveTextContent(data.stoDetails?.high?.toString() as string)
expect(securityTestsCard).toHaveTextContent(data.stoDetails?.medium?.toString() as string)
expect(securityTestsCard).toHaveTextContent(data.stoDetails?.low?.toString() as string)
+ await userEvent.click(securityTestsCard)
+ await waitFor(() => {
+ expect(mockHistoryPush).toHaveBeenCalledWith(
+ '/registries/1/artifacts/1/versions/1/pipelines/HARNESS_ARTIFACT_SCAN_PIPELINE/executions/Tbi7s6nETjmOMKU3Qrnm7A/security_tests?digest=sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d'
+ )
+ })
+ })
+
+ test('Verify action button on ssca card', async () => {
+ const { container } = render(
+
+
+
+ )
+
+ expect(getByTestId(container, 'integration-cards')).toBeInTheDocument()
+
+ const sscaCard = getByTestId(container, 'integration-supply-chain-card')
+ const downloadSbomBtn = getByText(sscaCard, 'versionDetails.cards.supplyChain.downloadSbom')
+ expect(downloadSbomBtn).toBeInTheDocument()
+ await userEvent.click(downloadSbomBtn)
+ await waitFor(() => {
+ expect(downloadSbom).toHaveBeenCalledWith({ 'orchestration-id': 'yw0D70fiTqetxx0HIyvEUQ', org: '', project: '' })
+ })
+
+ // empty response scenario
+ ;(downloadSbom as jest.Mock).mockImplementationOnce(
+ () =>
+ new Promise(success => {
+ success({ content: { sbom: null } })
+ })
+ )
+ await userEvent.click(downloadSbomBtn)
+ await waitFor(() => {
+ expect(showErrorToast).toHaveBeenCalledWith('versionDetails.cards.supplyChain.SBOMDataNotAvailable')
+ })
+
+ // error scenario
+ ;(downloadSbom as jest.Mock).mockImplementationOnce(
+ () =>
+ new Promise((_, reject) => {
+ reject({ message: 'error message' })
+ })
+ )
+ await userEvent.click(downloadSbomBtn)
+ await waitFor(() => {
+ expect(showErrorToast).toHaveBeenCalledWith('error message')
+ })
})
test('should show error message if failed to load integration data', async () => {
diff --git a/web/src/ar/pages/version-details/DockerVersion/__tests__/__mockData__.ts b/web/src/ar/pages/version-details/DockerVersion/__tests__/__mockData__.ts
index a5e3bb9d2..4da6a2db3 100644
--- a/web/src/ar/pages/version-details/DockerVersion/__tests__/__mockData__.ts
+++ b/web/src/ar/pages/version-details/DockerVersion/__tests__/__mockData__.ts
@@ -18,6 +18,8 @@ import type {
ArtifactVersionSummaryResponseResponse,
DockerArtifactDetailIntegrationResponseResponse,
DockerArtifactDetailResponseResponse,
+ DockerArtifactManifestResponseResponse,
+ DockerLayersResponseResponse,
DockerManifestsResponseResponse,
ListArtifactVersion,
ListArtifactVersionResponseResponse
@@ -86,6 +88,15 @@ export const mockDockerVersionSummary: ArtifactVersionSummaryResponseResponse =
status: 'SUCCESS'
}
+export const mockDockerVersionSummaryWithoutSscaAndStoData: ArtifactVersionSummaryResponseResponse = {
+ data: {
+ imageName: 'maven-app',
+ packageType: 'DOCKER',
+ version: '1.0.0'
+ },
+ status: 'SUCCESS'
+}
+
export const mockDockerVersionList: ListArtifactVersionResponseResponse = {
data: {
artifactVersions: [
@@ -238,3 +249,80 @@ export const mockDockerArtifactIntegrationDetails: DockerArtifactDetailIntegrati
},
status: 'SUCCESS'
}
+
+export const mockDockerArtifactLayers: DockerLayersResponseResponse = {
+ data: {
+ digest: 'sha256:144cdab68a435424250fe06e9a4f8a5f6b6b8a8a55d257bc6ee77476a6ec520d',
+ layers: [
+ {
+ command: '/bin/sh -c #(nop) ADD file:6fef7a4ab2de57c438dad76949e7eb87cfb1ea6f45b0f2423f71188efdaa0d8e in /',
+ size: '40.07 MB'
+ },
+ {
+ command: '/bin/sh -c #(nop) CMD ["/bin/bash"]',
+ size: '0 B'
+ },
+ {
+ command:
+ '/bin/sh -c set -eux; \tmicrodnf install \t\tgzip \t\ttar \t\t\t\tbinutils \t\tfreetype fontconfig \t; \tmicrodnf clean all',
+ size: '13.63 MB'
+ },
+ {
+ command: '/bin/sh -c #(nop) ENV JAVA_HOME=/usr/java/openjdk-17',
+ size: '0 B'
+ },
+ {
+ command:
+ '/bin/sh -c #(nop) ENV PATH=/usr/java/openjdk-17/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
+ size: '0 B'
+ },
+ {
+ command: '/bin/sh -c #(nop) ENV LANG=C.UTF-8',
+ size: '0 B'
+ },
+ {
+ command: '/bin/sh -c #(nop) ENV JAVA_VERSION=17.0.2',
+ size: '0 B'
+ },
+ {
+ command:
+ '/bin/sh -c set -eux; \t\tarch="$(objdump="$(command -v objdump)" \u0026\u0026 objdump --file-headers "$objdump" | awk -F \'[:,]+[[:space:]]+\' \'$1 == "architecture" { print $2 }\')"; \tcase "$arch" in \t\t\'i386:x86-64\') \t\t\tdownloadUrl=\'https://download.java.net/java/GA/jdk17.0.2/dfd4a8d0985749f896bed50d7138ee7f/8/GPL/openjdk-17.0.2_linux-x64_bin.tar.gz\'; \t\t\tdownloadSha256=\'0022753d0cceecacdd3a795dd4cea2bd7ffdf9dc06e22ffd1be98411742fbb44\'; \t\t\t;; \t\t\'aarch64\') \t\t\tdownloadUrl=\'https://download.java.net/java/GA/jdk17.0.2/dfd4a8d0985749f896bed50d7138ee7f/8/GPL/openjdk-17.0.2_linux-aarch64_bin.tar.gz\'; \t\t\tdownloadSha256=\'13bfd976acf8803f862e82c7113fb0e9311ca5458b1decaef8a09ffd91119fa4\'; \t\t\t;; \t\t*) echo \u003e\u00262 "error: unsupported architecture: \'$arch\'"; exit 1 ;; \tesac; \t\tcurl -fL -o openjdk.tgz "$downloadUrl"; \techo "$downloadSha256 *openjdk.tgz" | sha256sum --strict --check -; \t\tmkdir -p "$JAVA_HOME"; \ttar --extract \t\t--file openjdk.tgz \t\t--directory "$JAVA_HOME" \t\t--strip-components 1 \t\t--no-same-owner \t; \trm openjdk.tgz*; \t\trm -rf "$JAVA_HOME/lib/security/cacerts"; \tln -sT /etc/pki/ca-trust/extracted/java/cacerts "$JAVA_HOME/lib/security/cacerts"; \t\tln -sfT "$JAVA_HOME" /usr/java/default; \tln -sfT "$JAVA_HOME" /usr/java/latest; \tfor bin in "$JAVA_HOME/bin/"*; do \t\tbase="$(basename "$bin")"; \t\t[ ! -e "/usr/bin/$base" ]; \t\talternatives --install "/usr/bin/$base" "$base" "$bin" 20000; \tdone; \t\tjava -Xshare:dump; \t\tfileEncoding="$(echo \'System.out.println(System.getProperty("file.encoding"))\' | jshell -s -)"; [ "$fileEncoding" = \'UTF-8\' ]; rm -rf ~/.java; \tjavac --version; \tjava --version',
+ size: '177.73 MB'
+ },
+ {
+ command: '/bin/sh -c #(nop) CMD ["jshell"]',
+ size: '0 B'
+ },
+ {
+ command: 'WORKDIR /app',
+ size: '93 B'
+ },
+ {
+ command: 'COPY target/mavensampleapp-1.0.0.jar app.jar # buildkit',
+ size: '14.98 MB'
+ },
+ {
+ command: 'EXPOSE map[8080/tcp:{}]',
+ size: '0 B'
+ },
+ {
+ command: 'ENTRYPOINT ["java" "-jar" "app.jar"]',
+ size: '0 B'
+ }
+ ],
+ osArch: 'linux/arm64'
+ },
+ status: 'SUCCESS'
+}
+
+export const mockDockerArtifactManifest: DockerArtifactManifestResponseResponse = {
+ data: {
+ manifest:
+ '{\n "schemaVersion": 2,\n "mediaType": "application/vnd.docker.distribution.manifest.v2+json",\n "config": {\n "mediaType": "application/vnd.docker.container.image.v1+json",\n "digest": "sha256:f152582e19dbda7e3fb677e83af07fb60b05a56757b15edd8915aef2191aad62",\n "size": 4682\n },\n "layers": [\n {\n "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",\n "digest": "sha256:416105dc84fc8cf66df5d2c9f81570a2cc36a6cae58aedd4d58792f041f7a2f5",\n "size": 42018977\n },\n {\n "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",\n "digest": "sha256:fe66142579ff5bb0bb5cf989222e2bc77a97dcbd0283887dec04d5b9dfd48cfa",\n "size": 14294224\n },\n {\n "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",\n "digest": "sha256:1250d2aa493e8744c8f6cb528c8a882c14b6d7ff0af6862bbbfe676f60ea979e",\n "size": 186363988\n },\n {\n "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",\n "digest": "sha256:6a92a853917fafa754249a4f309e5c34caae0ee5df4369dc1c9383d7cd1b395e",\n "size": 93\n },\n {\n "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",\n "digest": "sha256:b2e80a78034b601c2513a1b00947bda6c6cda46a95e217b954ed84f5b1b5c0fe",\n "size": 15712414\n }\n ]\n}'
+ },
+ status: 'SUCCESS'
+}
+
+export const mockDockerSbomData = {
+ sbom: 'Test Data'
+}
diff --git a/web/src/ar/pages/version-details/GenericVersion/__tests__/GenericVersionArtifactDetailsPage.test.tsx b/web/src/ar/pages/version-details/GenericVersion/__tests__/GenericVersionArtifactDetailsPage.test.tsx
new file mode 100644
index 000000000..b26304fd6
--- /dev/null
+++ b/web/src/ar/pages/version-details/GenericVersion/__tests__/GenericVersionArtifactDetailsPage.test.tsx
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2024 Harness, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react'
+import copy from 'clipboard-copy'
+import userEvent from '@testing-library/user-event'
+import { useGetArtifactFilesQuery } from '@harnessio/react-har-service-client'
+import { getByText, queryByText, render, waitFor } from '@testing-library/react'
+
+import { getTableColumn } from '@ar/utils/testUtils/utils'
+import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
+
+import VersionDetailsPage from '../../VersionDetailsPage'
+import { mockGenericArtifactFiles, mockGenericVersionList, mockGenericVersionSummary } from './__mockData__'
+
+const mockHistoryPush = jest.fn()
+// eslint-disable-next-line jest-no-mock
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory: () => ({
+ push: mockHistoryPush
+ })
+}))
+
+jest.mock('clipboard-copy', () => ({
+ __esModule: true,
+ default: jest.fn()
+}))
+
+jest.mock('@harnessio/react-har-service-client', () => ({
+ useGetArtifactVersionSummaryQuery: jest.fn().mockImplementation(() => ({
+ data: { content: mockGenericVersionSummary },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ })),
+ getAllArtifactVersions: jest.fn().mockImplementation(
+ () =>
+ new Promise(success => {
+ success({ content: mockGenericVersionList })
+ })
+ ),
+ useGetArtifactFilesQuery: jest.fn().mockImplementation(() => ({
+ data: { content: mockGenericArtifactFiles },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ }))
+}))
+
+describe('Verify Generic Artifact Version Artifact Details Tab', () => {
+ test('verify file list table', async () => {
+ const { container } = render(
+
+
+
+ )
+
+ expect(container).toBeInTheDocument()
+
+ const getTableRowColumn = (row: number, col: number) => getTableColumn(row, col) as HTMLElement
+
+ const data = mockGenericArtifactFiles.files[0]
+ const row = 1
+ expect(getTableRowColumn(row, 1)).toHaveTextContent(data.name)
+ expect(getTableRowColumn(row, 2)).toHaveTextContent(data?.size as string)
+
+ const column3Content = getTableRowColumn(row, 3)
+ data.checksums.forEach(async each => {
+ const [checksum, value] = each.split(': ')
+ const btn = getByText(column3Content, `copy ${checksum}`)
+ await userEvent.click(btn)
+ expect(copy).toHaveBeenCalledWith(value)
+ })
+
+ const copyColumn = getTableRowColumn(row, 4)
+ expect(copyColumn).toHaveTextContent('copy')
+ const copyBtn = copyColumn.querySelector('[data-icon="code-copy"]') as HTMLElement
+ expect(copyBtn).toBeInTheDocument()
+ await userEvent.click(copyBtn)
+ expect(copy).toHaveBeenCalledWith(data?.downloadCommand)
+ })
+
+ test('verify pagination and sorting actions', async () => {
+ const { container } = render(
+
+
+
+ )
+ const nextPageBtn = getByText(container, 'Next')
+ await userEvent.click(nextPageBtn)
+
+ expect(useGetArtifactFilesQuery).toHaveBeenLastCalledWith({
+ artifact: '1/+',
+ queryParams: { page: 0, size: 50, sort_field: 'updatedAt', sort_order: 'DESC' },
+ registry_ref: 'undefined/1/+',
+ version: '1'
+ })
+
+ const fileNameSortIcon = getByText(container, 'versionDetails.artifactFiles.table.columns.name').nextSibling
+ ?.firstChild as HTMLElement
+ await userEvent.click(fileNameSortIcon)
+
+ await waitFor(() => {
+ expect(useGetArtifactFilesQuery).toHaveBeenLastCalledWith({
+ artifact: '1/+',
+ queryParams: { page: 0, size: 50, sort_field: 'updatedAt', sort_order: 'DESC' },
+ registry_ref: 'undefined/1/+',
+ version: '1'
+ })
+ })
+ })
+
+ test('verify error message', async () => {
+ const mockRefetchFn = jest.fn().mockImplementation(() => undefined)
+ ;(useGetArtifactFilesQuery as jest.Mock).mockImplementationOnce(() => {
+ return {
+ data: null,
+ loading: false,
+ error: { message: 'error message' },
+ refetch: mockRefetchFn
+ }
+ })
+
+ const { container } = render(
+
+
+
+ )
+
+ const errorText = getByText(container, 'error message')
+ expect(errorText).toBeInTheDocument()
+
+ const retryBtn = getByText(container, 'Retry')
+ expect(retryBtn).toBeInTheDocument()
+
+ await userEvent.click(retryBtn)
+ expect(mockRefetchFn).toHaveBeenCalled()
+ })
+
+ test('verify with empty pagination response', async () => {
+ const mockRefetchFn = jest.fn().mockImplementation(() => undefined)
+ ;(useGetArtifactFilesQuery as jest.Mock).mockImplementationOnce(() => {
+ return {
+ data: { content: { files: mockGenericArtifactFiles.files } },
+ loading: false,
+ error: null,
+ refetch: mockRefetchFn
+ }
+ })
+
+ const { container } = render(
+
+
+
+ )
+
+ const nextPageBtn = queryByText(container, 'Next')
+ expect(nextPageBtn).not.toBeInTheDocument()
+ })
+})
diff --git a/web/src/ar/pages/version-details/GenericVersion/__tests__/__mockData__.ts b/web/src/ar/pages/version-details/GenericVersion/__tests__/__mockData__.ts
index 8d376ae2b..458af733b 100644
--- a/web/src/ar/pages/version-details/GenericVersion/__tests__/__mockData__.ts
+++ b/web/src/ar/pages/version-details/GenericVersion/__tests__/__mockData__.ts
@@ -17,6 +17,8 @@
import type {
ArtifactDetailResponseResponse,
ArtifactVersionSummaryResponseResponse,
+ FileDetail,
+ FileDetailResponseResponse,
ListArtifactVersion,
ListArtifactVersionResponseResponse
} from '@harnessio/react-har-service-client'
@@ -119,3 +121,31 @@ export const mockGenericArtifactDetails: ArtifactDetailResponseResponse = {
},
status: 'SUCCESS'
}
+
+export const mockGenericArtifactFiles: FileDetailResponseResponse = {
+ files: [
+ {
+ checksums: [
+ 'SHA-512: ebfd0613c1c298d97fd7743ecd731b96a9c0a7f7cdbfdd9f19ec4682f9b4ceb400420a6191c9671bfb3e1cc5a9fef873ea1ad78f1b30794989a0fdb591f847cd',
+ 'SHA-256: cc5ac7831841b65901c5578a47d6b125259f9a4364d1d73b0b5d8891ad3b7d34',
+ 'SHA-1: b0e3200eb5eaca07d773916e306cd1ed9866d3a4',
+ 'MD5: cc576cbab9119ad7589cae7b57146af6'
+ ],
+ createdAt: '1738258177381',
+ downloadCommand:
+ "curl --location 'https://pkg.qa.harness.io/generic/iWnhltqOT7GFt7R-F_zP7Q/generic-registry/artifact:v1:image.png' --header 'x-api-key: \u003cAPI_KEY\u003e' -J -O",
+ name: 'image.png',
+ size: '170.18KB'
+ },
+ {
+ createdAt: '1738085520008',
+ name: 'hello.yaml',
+ size: '2.79MB'
+ } as FileDetail
+ ],
+ itemCount: 2,
+ pageCount: 4,
+ pageIndex: 0,
+ pageSize: 50,
+ status: 'SUCCESS'
+}
diff --git a/web/src/ar/pages/version-details/GenericVersion/pages/deployments/GenericArtifactDeploymentsPage.tsx b/web/src/ar/pages/version-details/GenericVersion/pages/deployments/GenericArtifactDeploymentsPage.tsx
deleted file mode 100644
index 010dff554..000000000
--- a/web/src/ar/pages/version-details/GenericVersion/pages/deployments/GenericArtifactDeploymentsPage.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2024 Harness, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React from 'react'
-import DeploymentsContent from '@ar/pages/version-details/components/DeploymentsContent/DeploymentsContent'
-
-export default function GenericArtifactDeploymentsPage() {
- return
-}
diff --git a/web/src/ar/pages/version-details/HelmVersion/__tests__/HelmVersionArtifactDetailsPage.test.tsx b/web/src/ar/pages/version-details/HelmVersion/__tests__/HelmVersionArtifactDetailsPage.test.tsx
new file mode 100644
index 000000000..44cfb082b
--- /dev/null
+++ b/web/src/ar/pages/version-details/HelmVersion/__tests__/HelmVersionArtifactDetailsPage.test.tsx
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2024 Harness, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react'
+import copy from 'clipboard-copy'
+import userEvent from '@testing-library/user-event'
+import { getByText, render, waitFor } from '@testing-library/react'
+import { useGetHelmArtifactManifestQuery } from '@harnessio/react-har-service-client'
+
+import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
+
+import { prettifyManifestJSON } from '../../utils'
+import VersionDetailsPage from '../../VersionDetailsPage'
+import { mockHelmArtifactManifest, mockHelmVersionList, mockHelmVersionSummary } from './__mockData__'
+
+const mockHistoryPush = jest.fn()
+// eslint-disable-next-line jest-no-mock
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory: () => ({
+ push: mockHistoryPush
+ })
+}))
+
+jest.mock('clipboard-copy', () => ({
+ __esModule: true,
+ default: jest.fn()
+}))
+
+jest.mock('@harnessio/react-har-service-client', () => ({
+ useGetArtifactVersionSummaryQuery: jest.fn().mockImplementation(() => ({
+ data: { content: mockHelmVersionSummary },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ })),
+ getAllArtifactVersions: jest.fn().mockImplementation(
+ () =>
+ new Promise(success => {
+ success({ content: mockHelmVersionList })
+ })
+ ),
+ useGetHelmArtifactManifestQuery: jest.fn().mockImplementation(() => ({
+ data: { content: mockHelmArtifactManifest },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ }))
+}))
+
+describe('Verify Helm Version Artifact Details Tab', () => {
+ beforeAll(() => {
+ jest.clearAllMocks()
+ })
+
+ test('Verify Manifest', async () => {
+ const { container } = render(
+
+
+
+ )
+
+ expect(container).toHaveTextContent('versionDetails.artifactDetails.tabs.manifest')
+
+ const copyBtn = container.querySelector('[data-icon="code-copy"]') as HTMLElement
+ expect(copyBtn).toBeInTheDocument()
+ await userEvent.click(copyBtn)
+ expect(copy).toHaveBeenCalledWith(prettifyManifestJSON(mockHelmArtifactManifest.data.manifest))
+ })
+
+ test('should show error message if failed to load manifest data', async () => {
+ const refetchFn = jest.fn()
+ ;(useGetHelmArtifactManifestQuery as jest.Mock).mockImplementation(() => ({
+ data: null,
+ error: { message: 'Failed to fetch artifact details' },
+ isLoading: false,
+ refetch: refetchFn
+ }))
+ const { container } = render(
+
+
+
+ )
+ expect(getByText(container, 'Failed to fetch artifact details')).toBeInTheDocument()
+ const retryBtn = container.querySelector('button[aria-label=Retry]')
+ expect(retryBtn).toBeInTheDocument()
+ await userEvent.click(retryBtn!)
+ await waitFor(() => {
+ expect(refetchFn).toHaveBeenCalled()
+ })
+ })
+})
diff --git a/web/src/ar/pages/version-details/HelmVersion/__tests__/__mockData__.ts b/web/src/ar/pages/version-details/HelmVersion/__tests__/__mockData__.ts
index 3b2618450..67031bcb0 100644
--- a/web/src/ar/pages/version-details/HelmVersion/__tests__/__mockData__.ts
+++ b/web/src/ar/pages/version-details/HelmVersion/__tests__/__mockData__.ts
@@ -17,6 +17,7 @@
import type {
ArtifactVersionSummaryResponseResponse,
HelmArtifactDetailResponseResponse,
+ HelmArtifactManifestResponseResponse,
ListArtifactVersion,
ListArtifactVersionResponseResponse
} from '@harnessio/react-har-service-client'
@@ -128,3 +129,11 @@ export const mockHelmArtifactDetails: HelmArtifactDetailResponseResponse = {
},
status: 'SUCCESS'
}
+
+export const mockHelmArtifactManifest: HelmArtifactManifestResponseResponse = {
+ data: {
+ manifest:
+ '{"schemaVersion":2,"config":{"mediaType":"application/vnd.cncf.helm.config.v1+json","digest":"sha256:3d3ca122368140982b0a494f4f357fff2fa9894f9adcc8809fa8e74e2a327d94","size":161},"layers":[{"mediaType":"application/vnd.cncf.helm.chart.content.v1.tar+gzip","digest":"sha256:a8b12f90950f22927e8c2e4f3e9b32655ae5287e95ae801662cef7cf66bd9be3","size":8062}],"annotations":{"org.opencontainers.image.created":"2024-10-25T18:43:34+05:30","org.opencontainers.image.description":"A Helm chart for deploying harness-delegate","org.opencontainers.image.title":"production","org.opencontainers.image.version":"1.0.15"}}'
+ },
+ status: 'SUCCESS'
+}
diff --git a/web/src/ar/pages/version-details/HelmVersion/components/OverviewCards/OverviewCards.tsx b/web/src/ar/pages/version-details/HelmVersion/components/OverviewCards/OverviewCards.tsx
deleted file mode 100644
index d30d354cc..000000000
--- a/web/src/ar/pages/version-details/HelmVersion/components/OverviewCards/OverviewCards.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2024 Harness, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import React from 'react'
-import { Layout } from '@harnessio/uicore'
-
-import DeploymentsCard from '@ar/pages/version-details/components/DeploymentsCard/DeploymentsCard'
-
-export default function HelmVersionOverviewCards() {
- return (
-
-
-
- )
-}
diff --git a/web/src/ar/pages/version-details/MavenVersion/__tests__/MavenVersionArtifactDetailsPage.test.tsx b/web/src/ar/pages/version-details/MavenVersion/__tests__/MavenVersionArtifactDetailsPage.test.tsx
new file mode 100644
index 000000000..3ee743b39
--- /dev/null
+++ b/web/src/ar/pages/version-details/MavenVersion/__tests__/MavenVersionArtifactDetailsPage.test.tsx
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2024 Harness, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react'
+import copy from 'clipboard-copy'
+import userEvent from '@testing-library/user-event'
+import { getByText, render } from '@testing-library/react'
+import { useGetArtifactFilesQuery } from '@harnessio/react-har-service-client'
+
+import { getTableColumn } from '@ar/utils/testUtils/utils'
+import ArTestWrapper from '@ar/utils/testUtils/ArTestWrapper'
+
+import VersionDetailsPage from '../../VersionDetailsPage'
+import { mockMavenArtifactFiles, mockMavenVersionList, mockMavenVersionSummary } from './__mockData__'
+
+const mockHistoryPush = jest.fn()
+// eslint-disable-next-line jest-no-mock
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useHistory: () => ({
+ push: mockHistoryPush
+ })
+}))
+
+jest.mock('clipboard-copy', () => ({
+ __esModule: true,
+ default: jest.fn()
+}))
+
+jest.mock('@harnessio/react-har-service-client', () => ({
+ useGetArtifactVersionSummaryQuery: jest.fn().mockImplementation(() => ({
+ data: { content: mockMavenVersionSummary },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ })),
+ getAllArtifactVersions: jest.fn().mockImplementation(
+ () =>
+ new Promise(success => {
+ success({ content: mockMavenVersionList })
+ })
+ ),
+ useGetArtifactFilesQuery: jest.fn().mockImplementation(() => ({
+ data: { content: mockMavenArtifactFiles },
+ error: null,
+ isLoading: false,
+ refetch: jest.fn()
+ }))
+}))
+
+describe('Verify Maven Artifact Version Artifact Details Tab', () => {
+ test('verify file list table', async () => {
+ const { container } = render(
+
+
+
+ )
+
+ expect(container).toBeInTheDocument()
+
+ const getTableRowColumn = (row: number, col: number) => getTableColumn(row, col) as HTMLElement
+
+ const data = mockMavenArtifactFiles.files[0]
+ const row = 1
+ expect(getTableRowColumn(row, 1)).toHaveTextContent(data.name)
+ expect(getTableRowColumn(row, 2)).toHaveTextContent(data?.size as string)
+
+ const column3Content = getTableRowColumn(row, 3)
+ data.checksums.forEach(async each => {
+ const [checksum, value] = each.split(': ')
+ const btn = getByText(column3Content, `copy ${checksum}`)
+ await userEvent.click(btn)
+ expect(copy).toHaveBeenCalledWith(value)
+ })
+
+ const copyColumn = getTableRowColumn(row, 4)
+ expect(copyColumn).toHaveTextContent('copy')
+ const copyBtn = copyColumn.querySelector('[data-icon="code-copy"]') as HTMLElement
+ expect(copyBtn).toBeInTheDocument()
+ await userEvent.click(copyBtn)
+ expect(copy).toHaveBeenCalledWith(data?.downloadCommand)
+ })
+
+ test('verify pagination and sorting actions', async () => {
+ const { container } = render(
+
+
+
+ )
+ const nextPageBtn = getByText(container, 'Next')
+ await userEvent.click(nextPageBtn)
+
+ expect(useGetArtifactFilesQuery).toHaveBeenLastCalledWith({
+ artifact: '1/+',
+ queryParams: { page: 0, size: 50, sort_field: 'updatedAt', sort_order: 'DESC' },
+ registry_ref: 'undefined/1/+',
+ version: '1'
+ })
+
+ const fileNameSortIcon = getByText(container, 'versionDetails.artifactFiles.table.columns.name').nextSibling
+ ?.firstChild as HTMLElement
+ await userEvent.click(fileNameSortIcon)
+
+ expect(useGetArtifactFilesQuery).toHaveBeenLastCalledWith({
+ artifact: '1/+',
+ queryParams: { page: 0, size: 50, sort_field: 'updatedAt', sort_order: 'DESC' },
+ registry_ref: 'undefined/1/+',
+ version: '1'
+ })
+ })
+
+ test('verify error message', async () => {
+ const mockRefetchFn = jest.fn().mockImplementation(() => undefined)
+ ;(useGetArtifactFilesQuery as jest.Mock).mockImplementationOnce(() => {
+ return {
+ data: null,
+ loading: false,
+ error: { message: 'error message' },
+ refetch: mockRefetchFn
+ }
+ })
+
+ const { container } = render(
+
+
+
+ )
+
+ const errorText = getByText(container, 'error message')
+ expect(errorText).toBeInTheDocument()
+
+ const retryBtn = getByText(container, 'Retry')
+ expect(retryBtn).toBeInTheDocument()
+
+ await userEvent.click(retryBtn)
+ expect(mockRefetchFn).toHaveBeenCalled()
+ })
+})
diff --git a/web/src/ar/pages/version-details/MavenVersion/__tests__/__mockData__.ts b/web/src/ar/pages/version-details/MavenVersion/__tests__/__mockData__.ts
index ba695ac93..c081b2b70 100644
--- a/web/src/ar/pages/version-details/MavenVersion/__tests__/__mockData__.ts
+++ b/web/src/ar/pages/version-details/MavenVersion/__tests__/__mockData__.ts
@@ -17,6 +17,7 @@
import type {
ArtifactDetailResponseResponse,
ArtifactVersionSummaryResponseResponse,
+ FileDetailResponseResponse,
ListArtifactVersion,
ListArtifactVersionResponseResponse
} from '@harnessio/react-har-service-client'
@@ -121,3 +122,65 @@ export const mockMavenArtifactDetails: ArtifactDetailResponseResponse = {
},
status: 'SUCCESS'
}
+
+export const mockMavenArtifactFiles: FileDetailResponseResponse = {
+ files: [
+ {
+ checksums: [
+ 'SHA-512: 43b206d0651f2748d685c9ed63942091fe529a0c838effeb15b3d21139a5c25f1086bad9e2df57a3032bf1cf26837b7cfe504f7b033d872a6d2d41d311aba882',
+ 'SHA-256: 04cf2f4ad947a81667690d64059ee29d3edc0d74649f82df489565c6cc1edcc0',
+ 'SHA-1: 475741bfe798caedd27a1c2580ea211aeba32521',
+ 'MD5: 5eb4f955706b83204f7d4dbbdecdb0e6'
+ ],
+ createdAt: '1738316037624',
+ downloadCommand:
+ "curl --location 'https://pkg.qa.harness.io/maven/iWnhltqOT7GFt7R-F_zP7Q/maven-up-1/junit/junit/3.8.1/junit-3.8.1.jar.sha1' --header 'x-api-key: \u003cIDENTITY_TOKEN\u003e' -O",
+ name: 'junit-3.8.1.jar.sha1',
+ size: '40.00B'
+ },
+ {
+ checksums: [
+ 'SHA-512: cfc89ca8303af5c04c75a73db181b61a34371b9e0dcc59e4d746190ac2e7636f0b257303ebef4db9a2cd980d192ab8879c91d84682d472b03fd3b9a732f184b6',
+ 'SHA-256: ab9b2ba5775492d85d45240f6f12e5880eb0ce26385fd80a1083e3b4ded402c2',
+ 'SHA-1: 11c996e14e70c07f6758f325838ea07e3bdf0742',
+ 'MD5: 4f215459aacbaaac97d02c29c41b2a57'
+ ],
+ createdAt: '1738316032125',
+ downloadCommand:
+ "curl --location 'https://pkg.qa.harness.io/maven/iWnhltqOT7GFt7R-F_zP7Q/maven-up-1/junit/junit/3.8.1/junit-3.8.1.pom.sha1' --header 'x-api-key: \u003cIDENTITY_TOKEN\u003e' -O",
+ name: 'junit-3.8.1.pom.sha1',
+ size: '58.00B'
+ },
+ {
+ checksums: [
+ 'SHA-512: 8e6f9fa5eb3ba93a8b1b5a39e01a81c142b33078264dbd0a2030d60dd26735407249a12e66f5cdcab8056e93a5687124fe66e741c233b4c7a06cc8e49f82e98b',
+ 'SHA-256: b58e459509e190bed737f3592bc1950485322846cf10e78ded1d065153012d70',
+ 'SHA-1: 99129f16442844f6a4a11ae22fbbee40b14d774f',
+ 'MD5: 1f40fb782a4f2cf78f161d32670f7a3a'
+ ],
+ createdAt: '1738152520460',
+ downloadCommand:
+ "curl --location 'https://pkg.qa.harness.io/maven/iWnhltqOT7GFt7R-F_zP7Q/maven-up-1/junit/junit/3.8.1/junit-3.8.1.jar' --header 'x-api-key: \u003cIDENTITY_TOKEN\u003e' -O",
+ name: 'junit-3.8.1.jar',
+ size: '118.23KB'
+ },
+ {
+ checksums: [
+ 'SHA-512: d43bddd7228b108eab508871d64725a730f6f159b0cee0e25a62df61f5362dc4c3e7c3413b5562b22e20934b40b5d994c1b1f66fec0e1a340613913e05203396',
+ 'SHA-256: e68f33343d832398f3c8aa78afcd808d56b7c1020de4d3ad8ce47909095ee904',
+ 'SHA-1: 16d74791c801c89b0071b1680ea0bc85c93417bb',
+ 'MD5: 50b40cb7342f52b702e6337d5debf1ae'
+ ],
+ createdAt: '1738152517836',
+ downloadCommand:
+ "curl --location 'https://pkg.qa.harness.io/maven/iWnhltqOT7GFt7R-F_zP7Q/maven-up-1/junit/junit/3.8.1/junit-3.8.1.pom' --header 'x-api-key: \u003cIDENTITY_TOKEN\u003e' -O",
+ name: 'junit-3.8.1.pom',
+ size: '998.00B'
+ }
+ ],
+ itemCount: 4,
+ pageCount: 1,
+ pageIndex: 0,
+ pageSize: 50,
+ status: 'SUCCESS'
+}
diff --git a/web/src/ar/pages/version-details/components/VersionDetailsTabs/VersionDetailsTabs.tsx b/web/src/ar/pages/version-details/components/VersionDetailsTabs/VersionDetailsTabs.tsx
index 0bdb4f6d3..4ad0120ee 100644
--- a/web/src/ar/pages/version-details/components/VersionDetailsTabs/VersionDetailsTabs.tsx
+++ b/web/src/ar/pages/version-details/components/VersionDetailsTabs/VersionDetailsTabs.tsx
@@ -39,7 +39,7 @@ import type { DockerVersionDetailsQueryParams } from '../../DockerVersion/types'
import css from './VersionDetailsTab.module.scss'
export default function VersionDetailsTabs(): JSX.Element {
- const [tab, setTab] = useState(VersionDetailsTab.OVERVIEW)
+ const [tab, setTab] = useState('')
const routes = useRoutes()
const history = useHistory()
@@ -109,7 +109,11 @@ export default function VersionDetailsTabs(): JSX.Element {
routeDefinitions.toARVersionDetailsTab({ ...versionDetailsTabWithSSCADetailsPathParams }),
routeDefinitions.toARVersionDetailsTab({ ...versionDetailsTabWithPipelineDetailsPathParams })
]}>
-
+
>