From 90382ef566b2d15e290e552ae49497643c8edbba Mon Sep 17 00:00:00 2001 From: Nick Chung Date: Tue, 16 Sep 2025 16:45:59 +0800 Subject: [PATCH] chore: add unit test for VexManagement, Dashboard, and ImageDetails --- .../components/__tests__/ImageDetails.spec.ts | 375 ++++++++++++++++++ .../__tests__/Dashboard.spec.ts | 146 +++++++ .../__tests__/VexManagement.spec.ts | 153 +++++++ 3 files changed, 674 insertions(+) create mode 100644 pkg/sbombastic-image-vulnerability-scanner/components/__tests__/ImageDetails.spec.ts create mode 100644 pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/__tests__/Dashboard.spec.ts create mode 100644 pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/__tests__/VexManagement.spec.ts diff --git a/pkg/sbombastic-image-vulnerability-scanner/components/__tests__/ImageDetails.spec.ts b/pkg/sbombastic-image-vulnerability-scanner/components/__tests__/ImageDetails.spec.ts new file mode 100644 index 0000000..6308a41 --- /dev/null +++ b/pkg/sbombastic-image-vulnerability-scanner/components/__tests__/ImageDetails.spec.ts @@ -0,0 +1,375 @@ +import { mount } from '@vue/test-utils'; +import { createStore } from 'vuex'; +import ImageDetails from '../ImageDetails.vue'; + +// Define constants locally to avoid import issues +const RESOURCE = { + IMAGE: "sbombastic.rancher.io.image" +}; + +describe('ImageDetails', () => { + let store: any; + let wrapper: any; + + const mockImageData = { + metadata: { name: 'test-image' }, + spec: { + repository: 'test-repo', + registry: 'test-registry', + scanResult: { + critical: 5, + high: 10, + medium: 15, + low: 8, + none: 2 + } + } + }; + + beforeEach(() => { + store = createStore({ + modules: { + cluster: { + namespaced: true, + getters: { + 'all': () => (type: string) => { + if (type === RESOURCE.IMAGE) return [mockImageData]; + return []; + } + }, + actions: { + 'findAll': jest.fn() + } + } + } + }); + + wrapper = mount(ImageDetails, { + global: { + plugins: [store], + mocks: { + $route: { + params: { + cluster: 'test-cluster', + id: 'test-image' + } + }, + $router: { + push: jest.fn() + }, + $t: (key: string) => key, + $store: store + }, + stubs: { + RouterLink: { + template: '', + props: ['to'] + }, + BadgeState: { + template: '', + props: ['color', 'label'] + }, + SortableTable: { + template: '
', + props: ['rows', 'headers', 'hasAdvancedFiltering', 'namespaced', 'rowActions', 'search'] + }, + ScoreBadge: { + template: '', + props: ['score', 'scoreType'] + }, + BarChart: { + template: '
', + props: ['chartData', 'description', 'colorPrefix'] + }, + Checkbox: { + template: '', + props: ['value', 'label'] + } + } + } + }); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + describe('Component Initialization', () => { + it('should render the component', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('should display the correct title with image name', () => { + const title = wrapper.find('.title'); + expect(title.exists()).toBe(true); + expect(title.text()).toContain('test-image'); + }); + + it('should display the severity badge', () => { + const severityBadge = wrapper.find('.severity-badge'); + expect(severityBadge.exists()).toBe(true); + }); + + it('should display the download report button', () => { + const downloadButton = wrapper.find('.btn.role-primary'); + expect(downloadButton.exists()).toBe(true); + expect(downloadButton.text()).toContain('imageScanner.images.downloadReport'); + }); + }); + + describe('Image Information Section', () => { + it('should display image details in info grid', () => { + const infoGrid = wrapper.find('.info-grid'); + expect(infoGrid.exists()).toBe(true); + }); + + it('should show basic image properties', () => { + const infoItems = wrapper.findAll('.info-item'); + expect(infoItems.length).toBeGreaterThan(0); + }); + + it('should toggle show all properties when clicked', async () => { + const showPropertiesLink = wrapper.find('.show-properties-link a'); + expect(showPropertiesLink.exists()).toBe(true); + + await showPropertiesLink.trigger('click'); + expect(wrapper.vm.showAllProperties).toBe(true); + + await showPropertiesLink.trigger('click'); + expect(wrapper.vm.showAllProperties).toBe(false); + }); + + it('should show additional properties when showAllProperties is true', async () => { + wrapper.vm.showAllProperties = true; + await wrapper.vm.$nextTick(); + + // These should be visible when showAllProperties is true + expect(wrapper.vm.showAllProperties).toBe(true); + + // Check that additional properties are rendered + const infoItems = wrapper.findAll('.info-item'); + expect(infoItems.length).toBeGreaterThan(10); // Should have more items when expanded + }); + }); + + describe('Summary Section', () => { + it('should render vulnerabilities section', () => { + const vulnerabilitiesSection = wrapper.find('.vulnerabilities-section'); + expect(vulnerabilitiesSection.exists()).toBe(true); + }); + + it('should render severity section with BarChart', () => { + const severitySection = wrapper.find('.severity-section'); + expect(severitySection.exists()).toBe(true); + + const barChart = wrapper.find('.bar-chart'); + expect(barChart.exists()).toBe(true); + }); + + it('should display most severe vulnerabilities', () => { + const vulnerabilitiesList = wrapper.find('.vulnerabilities-list'); + expect(vulnerabilitiesList.exists()).toBe(true); + }); + }); + + describe('Vulnerability Table', () => { + it('should render SortableTable component', () => { + const sortableTable = wrapper.find('.sortable-table'); + expect(sortableTable.exists()).toBe(true); + }); + + it('should display download custom report button', () => { + const downloadCustomButton = wrapper.find('.table-header-actions .btn.role-primary'); + expect(downloadCustomButton.exists()).toBe(true); + expect(downloadCustomButton.text()).toContain('imageScanner.images.buttons.downloadCustomReport'); + }); + + it('should show selected count when vulnerabilities are selected', async () => { + wrapper.vm.selectedVulnerabilities = ['CVE-2017-5337', 'CVE-2018-1000007']; + await wrapper.vm.$nextTick(); + + const selectedCount = wrapper.find('.selected-count'); + expect(selectedCount.exists()).toBe(true); + expect(selectedCount.text()).toContain('2'); + }); + }); + + describe('Computed Properties', () => { + it('should calculate total vulnerabilities correctly', () => { + wrapper.vm.severityDistribution = { + critical: 5, + high: 10, + medium: 15, + low: 8, + none: 2 + }; + + expect(wrapper.vm.totalVulnerabilities).toBe(40); + }); + + it('should determine overall severity correctly', () => { + wrapper.vm.severityDistribution = { + critical: 5, + high: 0, + medium: 0, + low: 0, + none: 0 + }; + + expect(wrapper.vm.overallSeverity).toBe('critical'); + }); + + it('should return none when no vulnerabilities', () => { + wrapper.vm.severityDistribution = { + critical: 0, + high: 0, + medium: 0, + low: 0, + none: 0 + }; + + expect(wrapper.vm.overallSeverity).toBe('none'); + }); + + it('should calculate severity distribution with percentages', () => { + wrapper.vm.severityDistribution = { + critical: 10, + high: 20, + medium: 0, + low: 0, + none: 0 + }; + + const distribution = wrapper.vm.severityDistributionWithPercentages; + expect(distribution.critical.percentage).toBe('33.3'); + expect(distribution.high.percentage).toBe('66.7'); + }); + + it('should filter vulnerabilities by CVE search', () => { + wrapper.vm.filters.cveSearch = 'CVE-2017'; + const filtered = wrapper.vm.filteredVulnerabilities; + expect(filtered.every(v => v.cveId.includes('CVE-2017'))).toBe(true); + }); + + it('should filter vulnerabilities by score', () => { + wrapper.vm.filters.score = '9.0'; + const filtered = wrapper.vm.filteredVulnerabilities; + expect(filtered.every(v => { + const score = parseFloat(v.score.split(' ')[0]); + return score >= 9.0; + })).toBe(true); + }); + + it('should filter vulnerabilities by package search', () => { + wrapper.vm.filters.packageSearch = 'tomcat'; + const filtered = wrapper.vm.filteredVulnerabilities; + expect(filtered.every(v => v.package.toLowerCase().includes('tomcat'))).toBe(true); + }); + + it('should filter vulnerabilities by fix availability', () => { + wrapper.vm.filters.fixAvailable = 'available'; + const filtered = wrapper.vm.filteredVulnerabilities; + expect(filtered.every(v => v.fixAvailable === true)).toBe(true); + }); + + it('should filter vulnerabilities by severity', () => { + wrapper.vm.filters.severity = 'critical'; + const filtered = wrapper.vm.filteredVulnerabilities; + expect(filtered.every(v => v.severity === 'critical')).toBe(true); + }); + + it('should filter vulnerabilities by exploitability', () => { + wrapper.vm.filters.exploitability = 'affected'; + const filtered = wrapper.vm.filteredVulnerabilities; + expect(filtered.every(v => v.exploitability === 'affected')).toBe(true); + }); + }); + + describe('Methods', () => { + it('should return correct severity color', () => { + expect(wrapper.vm.getSeverityColor('critical')).toBe('critical-severity'); + expect(wrapper.vm.getSeverityColor('high')).toBe('bg-error'); + expect(wrapper.vm.getSeverityColor('medium')).toBe('bg-warning'); + expect(wrapper.vm.getSeverityColor('low')).toBe('bg-warning'); + expect(wrapper.vm.getSeverityColor('none')).toBe('bg-info'); + }); + + it('should return correct severity bar color', () => { + expect(wrapper.vm.getSeverityBarColor('critical')).toBe('#850917'); + expect(wrapper.vm.getSeverityBarColor('high')).toBe('#DE2136'); + expect(wrapper.vm.getSeverityBarColor('medium')).toBe('#FF8533'); + expect(wrapper.vm.getSeverityBarColor('low')).toBe('#FDD835'); + expect(wrapper.vm.getSeverityBarColor('none')).toBe('#E0E0E0'); + }); + + it('should handle selection change', () => { + const selected = ['CVE-2017-5337', 'CVE-2018-1000007']; + wrapper.vm.onSelectionChange(selected); + expect(wrapper.vm.selectedVulnerabilities).toEqual(selected); + }); + + it('should handle download full report', () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + wrapper.vm.downloadFullReport(); + expect(consoleSpy).toHaveBeenCalledWith('Downloading full report for:', wrapper.vm.imageName); + consoleSpy.mockRestore(); + }); + + it('should handle download custom report', () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + wrapper.vm.downloadCustomReport(); + expect(consoleSpy).toHaveBeenCalledWith('Downloading custom report with filters:', wrapper.vm.filters); + consoleSpy.mockRestore(); + }); + + it('should calculate most severe vulnerabilities correctly', () => { + // Ensure vulnerability details are loaded first + wrapper.vm.vulnerabilityDetails = wrapper.vm.getMockVulnerabilityDetails(); + wrapper.vm.calculateMostSevereVulnerabilities(); + expect(wrapper.vm.mostSevereVulnerabilities).toHaveLength(5); + + // Should be sorted by score descending + const scores = wrapper.vm.mostSevereVulnerabilities.map(v => parseFloat(v.score.split(' ')[0])); + for (let i = 1; i < scores.length; i++) { + expect(scores[i-1]).toBeGreaterThanOrEqual(scores[i]); + } + }); + }); + + describe('Component Data', () => { + it('should have correct initial data properties', () => { + expect(wrapper.vm.PRODUCT_NAME).toBeDefined(); + expect(wrapper.vm.RESOURCE).toBeDefined(); + expect(wrapper.vm.PAGE).toBeDefined(); + expect(wrapper.vm.VULNERABILITY_DETAILS_TABLE).toBeDefined(); + expect(wrapper.vm.showAllProperties).toBe(false); + expect(wrapper.vm.selectedVulnerabilities).toEqual([]); + }); + + it('should have correct filter defaults', () => { + expect(wrapper.vm.filters.cveSearch).toBe(''); + expect(wrapper.vm.filters.score).toBe(''); + expect(wrapper.vm.filters.packageSearch).toBe(''); + expect(wrapper.vm.filters.fixAvailable).toBe('any'); + expect(wrapper.vm.filters.severity).toBe('any'); + expect(wrapper.vm.filters.exploitability).toBe('any'); + }); + }); + + describe('Mock Data', () => { + it('should generate mock vulnerability details', () => { + const mockData = wrapper.vm.getMockVulnerabilityDetails(); + expect(Array.isArray(mockData)).toBe(true); + expect(mockData.length).toBeGreaterThan(0); + + // Check structure of first vulnerability + const firstVuln = mockData[0]; + expect(firstVuln).toHaveProperty('cveId'); + expect(firstVuln).toHaveProperty('score'); + expect(firstVuln).toHaveProperty('package'); + expect(firstVuln).toHaveProperty('fixAvailable'); + expect(firstVuln).toHaveProperty('severity'); + expect(firstVuln).toHaveProperty('exploitability'); + }); + }); +}); \ No newline at end of file diff --git a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/__tests__/Dashboard.spec.ts b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/__tests__/Dashboard.spec.ts new file mode 100644 index 0000000..76d5c0e --- /dev/null +++ b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/__tests__/Dashboard.spec.ts @@ -0,0 +1,146 @@ +import { mount } from '@vue/test-utils'; +import { createStore } from 'vuex'; +import Dashboard from '../Dashboard.vue'; + +// Define RESOURCE constants locally to avoid import issues +const RESOURCE = { + REGISTRY: "sbombastic.rancher.io.registry", + SCAN_JOB: "sbombastic.rancher.io.scanjob" +}; + +describe('Dashboard', () => { + let store: any; + let wrapper: any; + + beforeEach(() => { + store = createStore({ + modules: { + cluster: { + namespaced: true, + getters: { + 'all': () => (type: string) => { + if (type === RESOURCE.REGISTRY) return []; + if (type === RESOURCE.SCAN_JOB) return []; + return []; + } + }, + actions: { + 'findAll': jest.fn() + } + } + } + }); + + wrapper = mount(Dashboard, { + global: { + plugins: [store], + mocks: { + $route: { + params: { cluster: 'test-cluster' } + }, + $router: { + push: jest.fn() + }, + $t: (key: string) => key, + $store: store + }, + stubs: { + RouterLink: { + template: '', + props: ['to'] + }, + LabeledSelect: { + template: '', + props: ['value', 'options', 'closeOnSelect', 'multiple'] + }, + SevereVulnerabilitiesItem: { + template: '
', + props: ['vulnerability'] + }, + TopSevereVulnerabilitiesChart: { + template: '
', + props: ['topSevereVulnerabilities'] + }, + ImageRiskAssessment: { + template: '
', + props: ['vulnerabilityStats', 'scanningStats', 'chartData'] + }, + TopRiskyImagesChart: { + template: '
', + props: ['topRiskyImages'] + } + } + } + }); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + describe('Component Initialization', () => { + it('should render the component', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('should display the correct title', () => { + expect(wrapper.find('.title').text()).toContain('imageScanner.dashboard.title'); + }); + + it('should display the download report button', () => { + const downloadButton = wrapper.find('.btn.role-primary'); + expect(downloadButton.exists()).toBe(true); + expect(downloadButton.text()).toContain('imageScanner.images.downloadReport'); + }); + + it('should display the registry filter dropdown', () => { + const labeledSelect = wrapper.find('.labeled-select'); + expect(labeledSelect.exists()).toBe(true); + }); + }); + + describe('Component Structure', () => { + it('should render ImageRiskAssessment component', () => { + const imageRiskAssessment = wrapper.find('.image-risk-assessment'); + expect(imageRiskAssessment.exists()).toBe(true); + }); + + it('should render TopSevereVulnerabilitiesChart component', () => { + const topSevereVulnerabilitiesChart = wrapper.find('.top-severe-vulnerabilities-chart'); + expect(topSevereVulnerabilitiesChart.exists()).toBe(true); + }); + + it('should render TopRiskyImagesChart component', () => { + const topRiskyImagesChart = wrapper.find('.top-risky-images-chart'); + expect(topRiskyImagesChart.exists()).toBe(true); + }); + }); + + describe('Actions', () => { + it('should handle download report action', () => { + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); + wrapper.vm.openAddEditRuleModal(); + expect(consoleSpy).toHaveBeenCalledWith('Download full report'); + consoleSpy.mockRestore(); + }); + + it('should handle refresh action', async () => { + const loadDashboardDataSpy = jest.spyOn(wrapper.vm, 'loadDashboardData'); + await wrapper.vm.refresh(); + expect(wrapper.vm.disabled).toBe(false); + expect(loadDashboardDataSpy).toHaveBeenCalled(); + loadDashboardDataSpy.mockRestore(); + }); + }); + + describe('Component Data', () => { + it('should have correct initial data properties', () => { + expect(wrapper.vm.PRODUCT_NAME).toBeDefined(); + expect(wrapper.vm.disabled).toBe(false); + expect(wrapper.vm.selectedRegistry).toBe('all'); + expect(wrapper.vm.registryOptions).toBeDefined(); + expect(wrapper.vm.vulnerabilityStats).toBeDefined(); + expect(wrapper.vm.scanningStats).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/__tests__/VexManagement.spec.ts b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/__tests__/VexManagement.spec.ts new file mode 100644 index 0000000..2ba62f5 --- /dev/null +++ b/pkg/sbombastic-image-vulnerability-scanner/pages/c/_cluster/sbombastic-image-vulnerability-scanner/__tests__/VexManagement.spec.ts @@ -0,0 +1,153 @@ +import { mount } from '@vue/test-utils'; +import { createStore } from 'vuex'; +import VexManagement from '../VexManagement.vue'; + +// Define RESOURCE constants locally to avoid import issues +const RESOURCE = { + VEX_HUB: "sbombastic.rancher.io.vexhub" +}; + +describe('VexManagement', () => { + let store: any; + let wrapper: any; + + const mockVexHubs = [ + { + id: 'vexhub-1', + metadata: { + name: 'vexhub-1', + creationTimestamp: '2024-01-15T10:30:00Z' + }, + spec: { + url: 'https://vex.example.com', + enabled: true + } + }, + { + id: 'vexhub-2', + metadata: { + name: 'vexhub-2', + creationTimestamp: '2024-01-16T14:20:00Z' + }, + spec: { + url: 'https://vex2.example.com', + enabled: false + } + } + ]; + + const mockSchema = { + id: 'sbombastic.rancher.io.vexhub', + type: 'schema', + links: { + collection: '/v1/sbombastic.rancher.io.vexhubs', + self: '/v1/schemas/sbombastic.rancher.io.vexhub' + }, + resourceMethods: ['GET', 'DELETE', 'PUT', 'PATCH'], + collectionMethods: ['GET', 'POST'], + attributes: { + group: 'sbombastic.rancher.io', + kind: 'VEXHub', + namespaced: false, + resource: 'vexhubs', + verbs: ['delete', 'deletecollection', 'get', 'list', 'patch', 'create', 'update', 'watch'], + version: 'v1alpha1' + } + }; + + beforeEach(() => { + store = createStore({ + modules: { + cluster: { + namespaced: true, + getters: { + 'all': () => (type: string) => { + if (type === RESOURCE.VEX_HUB) return mockVexHubs; + return []; + }, + 'schemaFor': () => (type: string) => { + if (type === RESOURCE.VEX_HUB) return mockSchema; + return null; + } + }, + actions: { + 'findAll': jest.fn() + } + } + } + }); + + wrapper = mount(VexManagement, { + global: { + plugins: [store], + mocks: { + $route: { + params: { cluster: 'test-cluster' } + }, + $router: { + push: jest.fn() + }, + $t: (key: string) => key, + $store: store + }, + stubs: { + RouterLink: { + template: '', + props: ['to'] + }, + VexHubList: { + template: '
VexHubList Component
', + name: 'VexHubList' + } + } + } + }); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + describe('Component Initialization', () => { + it('should render the component', () => { + expect(wrapper.exists()).toBe(true); + }); + + it('should display the correct title', () => { + expect(wrapper.find('.title').text()).toContain('imageScanner.vexManagement.title'); + }); + + it('should display the correct description', () => { + expect(wrapper.find('.description').text()).toContain('imageScanner.vexManagement.description'); + }); + + it('should display the create button', () => { + const createButton = wrapper.find('.btn.role-primary'); + expect(createButton.exists()).toBe(true); + expect(createButton.text()).toContain('imageScanner.vexManagement.button.create'); + }); + }); + + describe('Navigation', () => { + it('should navigate to create VEX hub page when create button is clicked', () => { + const createButton = wrapper.find('.btn.role-primary'); + createButton.trigger('click'); + + expect(wrapper.vm.$router.push).toHaveBeenCalledWith({ + name: 'imageScanner-c-cluster-resource-create', + params: { + resource: RESOURCE.VEX_HUB, + cluster: 'test-cluster', + product: 'imageScanner' + } + }); + }); + }); + + describe('Component Structure', () => { + it('should render VexHubList component', () => { + const vexHubList = wrapper.findComponent({ name: 'VexHubList' }); + expect(vexHubList.exists()).toBe(true); + }); + }); +}); \ No newline at end of file