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