import { HealthService } from '~/app/shared/api/health.service';
import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { HardwareNameMapping } from '~/app/shared/enum/hardware.enum';
-describe('OverviewStorageCardComponent (Jest)', () => {
+const MOCK_HW_SUMMARY = {
+ total: {
+ category: {
+ memory: { total: 8, ok: 8, error: 0 },
+ storage: { total: 4, ok: 3, error: 1 },
+ processors: { total: 2, ok: 2, error: 0 },
+ network: { total: 6, ok: 5, error: 1 },
+ power: { total: 4, ok: 4, error: 0 },
+ fans: { total: 12, ok: 10, error: 2 }
+ },
+ total: { total: 36, ok: 32, error: 4 }
+ },
+ host: { flawed: 2 }
+};
+
+const EXPECTED_ICON_MAP: Record<string, string> = {
+ memory: 'dataEnrichment',
+ storage: 'vmdkDisk',
+ processors: 'chip',
+ network: 'network1',
+ power: 'plug',
+ fans: 'ibmStreamSets'
+};
+
+describe('OverviewHealthCardComponent', () => {
let component: OverviewHealthCardComponent;
let fixture: ComponentFixture<OverviewHealthCardComponent>;
};
const mockAuthStorageService = {
- getPermissions: jest.fn(() => ({ configOpt: { read: false } }))
+ getPermissions: jest.fn(() => ({ configOpt: { read: true } }))
};
const mockMgrModuleService = {
- getConfig: jest.fn(() => of({ hw_monitoring: false }))
+ getConfig: jest.fn(() => of({ hw_monitoring: true }))
};
const mockHardwareService = {
- getSummary: jest.fn(() => of(null))
+ getSummary: jest.fn(() => of(MOCK_HW_SUMMARY))
};
const mockHealthService = {
it('should create', () => {
expect(component).toBeTruthy();
});
+
+ describe('hardware sections', () => {
+ it('should emit hardware rows grouped in pairs', (done) => {
+ component.sections$.subscribe((sections) => {
+ expect(sections).not.toBeNull();
+ sections!.forEach((section) => {
+ expect(section.length).toBeLessThanOrEqual(2);
+ });
+ done();
+ });
+ });
+
+ it('should limit to 3 groups (6 categories max)', (done) => {
+ component.sections$.subscribe((sections) => {
+ expect(sections).not.toBeNull();
+ expect(sections!.length).toBeLessThanOrEqual(3);
+ done();
+ });
+ });
+
+ it('should map each row to correct icon from HW_ICON_MAP', (done) => {
+ component.sections$.subscribe((sections) => {
+ const rows = sections!.flat();
+ rows.forEach((row) => {
+ expect(row.icon).toBe(EXPECTED_ICON_MAP[row.key]);
+ });
+ done();
+ });
+ });
+
+ it('should map each row to correct label from HardwareNameMapping', (done) => {
+ component.sections$.subscribe((sections) => {
+ const rows = sections!.flat();
+ rows.forEach((row) => {
+ expect(row.label).toBe(
+ HardwareNameMapping[row.key as keyof typeof HardwareNameMapping]
+ );
+ });
+ done();
+ });
+ });
+
+ it('should compute ok and error counts from summary data', (done) => {
+ component.sections$.subscribe((sections) => {
+ const rows = sections!.flat();
+ const memoryRow = rows.find((r) => r.key === 'memory');
+ expect(memoryRow?.ok).toBe(8);
+ expect(memoryRow?.error).toBe(0);
+
+ const storageRow = rows.find((r) => r.key === 'storage');
+ expect(storageRow?.ok).toBe(3);
+ expect(storageRow?.error).toBe(1);
+
+ const fansRow = rows.find((r) => r.key === 'fans');
+ expect(fansRow?.ok).toBe(10);
+ expect(fansRow?.error).toBe(2);
+ done();
+ });
+ });
+
+ it('should include exactly memory, storage, processors, network, power, fans', (done) => {
+ component.sections$.subscribe((sections) => {
+ const keys = sections!.flat().map((r) => r.key);
+ expect(keys).toEqual(['memory', 'storage', 'processors', 'network', 'power', 'fans']);
+ done();
+ });
+ });
+ });
+});
+
+describe('OverviewHealthCardComponent (hw disabled)', () => {
+ let component: OverviewHealthCardComponent;
+ let fixture: ComponentFixture<OverviewHealthCardComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ OverviewHealthCardComponent,
+ CommonModule,
+ ProductiveCardComponent,
+ SkeletonModule,
+ ButtonModule,
+ RouterModule,
+ ComponentsModule,
+ LinkModule,
+ PipesModule
+ ],
+ providers: [
+ {
+ provide: SummaryService,
+ useValue: {
+ summaryData$: of({
+ version: 'ceph version 18.0.0 reef (dev)'
+ })
+ }
+ },
+ { provide: UpgradeService, useValue: { listCached: jest.fn(() => of(null)) } },
+ {
+ provide: AuthStorageService,
+ useValue: { getPermissions: jest.fn(() => ({ configOpt: { read: true } })) }
+ },
+ { provide: MgrModuleService, useValue: { getConfig: jest.fn(() => of({ hw_monitoring: false })) } },
+ { provide: HardwareService, useValue: { getSummary: jest.fn(() => of(null)) } },
+ { provide: HealthService, useValue: { getTelemetryStatus: jest.fn(() => of(false)) } },
+ provideRouter([])
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(OverviewHealthCardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should emit null sections when hw monitoring is disabled', (done) => {
+ component.sections$.subscribe((sections) => {
+ expect(sections).toBeNull();
+ done();
+ });
+ });
+
+ it('should report enabled$ as false', (done) => {
+ component.enabled$.subscribe((enabled) => {
+ expect(enabled).toBe(false);
+ done();
+ });
+ });
+});
+
+describe('OverviewHealthCardComponent (no permissions)', () => {
+ let component: OverviewHealthCardComponent;
+ let fixture: ComponentFixture<OverviewHealthCardComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ OverviewHealthCardComponent,
+ CommonModule,
+ ProductiveCardComponent,
+ SkeletonModule,
+ ButtonModule,
+ RouterModule,
+ ComponentsModule,
+ LinkModule,
+ PipesModule
+ ],
+ providers: [
+ {
+ provide: SummaryService,
+ useValue: {
+ summaryData$: of({
+ version: 'ceph version 18.0.0 reef (dev)'
+ })
+ }
+ },
+ { provide: UpgradeService, useValue: { listCached: jest.fn(() => of(null)) } },
+ {
+ provide: AuthStorageService,
+ useValue: { getPermissions: jest.fn(() => ({ configOpt: { read: false } })) }
+ },
+ { provide: MgrModuleService, useValue: { getConfig: jest.fn(() => of({})) } },
+ { provide: HardwareService, useValue: { getSummary: jest.fn(() => of(null)) } },
+ { provide: HealthService, useValue: { getTelemetryStatus: jest.fn(() => of(false)) } },
+ provideRouter([])
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(OverviewHealthCardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should emit false for enabled$ when no configOpt read permission', (done) => {
+ component.enabled$.subscribe((enabled) => {
+ expect(enabled).toBe(false);
+ done();
+ });
+ });
});
--- /dev/null
+import unittest
+from unittest import mock
+
+from ..exceptions import DashboardException
+from ..services.hardware import HardwareService, STATUS_OK, STATUS_UNKNOWN
+
+
+MOCK_HARDWARE_DATA = {
+ 'memory': {
+ 'host1': {
+ 'SystemBoard': {
+ 'DIMM.Socket.A1': {
+ 'description': 'DIMM DDR5',
+ 'status': {'health': 'OK'}
+ },
+ 'DIMM.Socket.A2': {
+ 'description': 'DIMM DDR5',
+ 'status': {'health': 'OK'}
+ }
+ }
+ },
+ 'host2': {
+ 'SystemBoard': {
+ 'DIMM.Socket.A1': {
+ 'description': 'DIMM DDR5',
+ 'status': {'health': 'OK'}
+ }
+ }
+ }
+ },
+ 'storage': {
+ 'host1': {
+ 'RAID.Integrated.1': {
+ 'Disk.Bay.0': {
+ 'description': 'SSD 960GB',
+ 'status': {'health': 'OK'}
+ },
+ 'Disk.Bay.1': {
+ 'description': 'SSD 960GB',
+ 'status': {'health': 'Critical'}
+ }
+ }
+ }
+ },
+ 'processors': {
+ 'host1': {
+ 'SystemBoard': {
+ 'CPU.Socket.1': {
+ 'description': 'Intel Xeon',
+ 'status': {'health': 'OK'}
+ }
+ }
+ }
+ },
+ 'network': {
+ 'host1': {
+ 'SystemBoard': {
+ 'NIC.Slot.1': {
+ 'description': 'Ethernet 25G',
+ 'status': {'health': 'OK'}
+ }
+ }
+ }
+ },
+ 'power': {
+ 'host1': {
+ 'SystemBoard': {
+ 'PSU.Slot.1': {
+ 'description': 'PSU 750W',
+ 'status': {'health': 'OK'}
+ }
+ }
+ }
+ },
+ 'fans': {
+ 'host1': {
+ 'SystemBoard': {
+ 'Fan.Embedded.1': {
+ 'description': 'System Fan',
+ 'status': {'health': 'OK'}
+ }
+ }
+ }
+ }
+}
+
+MOCK_STRING_STATUS_DATA = {
+ 'host1': {
+ 'SystemBoard': {
+ 'DIMM.Socket.A1': {
+ 'description': 'DIMM DDR5',
+ 'status': 'OK'
+ },
+ 'DIMM.Socket.A2': {
+ 'description': 'DIMM DDR5',
+ 'status': 'Critical'
+ }
+ }
+ }
+}
+
+MOCK_MISSING_STATUS_DATA = {
+ 'host1': {
+ 'SystemBoard': {
+ 'DIMM.Socket.A1': {
+ 'description': 'DIMM DDR5'
+ }
+ }
+ }
+}
+
+MOCK_NON_DICT_COMPONENT = {
+ 'host1': {
+ 'SystemBoard': {
+ 'DIMM.Socket.A1': 'not-a-dict'
+ }
+ }
+}
+
+
+class HardwareConstantsTest(unittest.TestCase):
+ def test_status_ok_value(self):
+ self.assertEqual(STATUS_OK, 'OK')
+
+ def test_status_unknown_value(self):
+ self.assertEqual(STATUS_UNKNOWN, 'Unknown')
+
+
+class HardwareValidateCategoriesTest(unittest.TestCase):
+ def test_none_returns_all(self):
+ result = HardwareService.validate_categories(None)
+ self.assertEqual(
+ result,
+ ['memory', 'storage', 'processors', 'network',
+ 'power', 'fans', 'temperatures']
+ )
+
+ def test_single_string_wrapped_in_list(self):
+ result = HardwareService.validate_categories('memory')
+ self.assertEqual(result, ['memory'])
+
+ def test_valid_list_passes(self):
+ result = HardwareService.validate_categories(['memory', 'fans'])
+ self.assertEqual(result, ['memory', 'fans'])
+
+ def test_invalid_category_raises(self):
+ with self.assertRaises(DashboardException):
+ HardwareService.validate_categories(['nonexistent'])
+
+ def test_non_list_type_raises(self):
+ with self.assertRaises(DashboardException):
+ HardwareService.validate_categories(123)
+
+
+class HardwareGetSummaryTest(unittest.TestCase):
+ def _mock_common(self, data_map):
+ def side_effect(category, hostname=None):
+ return data_map.get(category, {})
+ return side_effect
+
+ @mock.patch('dashboard.services.hardware.OrchClient.instance')
+ def test_dict_status_health_ok(self, mock_instance):
+ fake_client = mock.Mock()
+ fake_client.hardware.common = self._mock_common(MOCK_HARDWARE_DATA)
+ mock_instance.return_value = fake_client
+
+ result = HardwareService.get_summary(categories=['memory'])
+ cat = result['total']['category']['memory']
+ self.assertEqual(cat['total'], 3)
+ self.assertEqual(cat['ok'], 3)
+ self.assertEqual(cat['error'], 0)
+
+ @mock.patch('dashboard.services.hardware.OrchClient.instance')
+ def test_dict_status_health_mixed(self, mock_instance):
+ fake_client = mock.Mock()
+ fake_client.hardware.common = self._mock_common(MOCK_HARDWARE_DATA)
+ mock_instance.return_value = fake_client
+
+ result = HardwareService.get_summary(categories=['storage'])
+ cat = result['total']['category']['storage']
+ self.assertEqual(cat['total'], 2)
+ self.assertEqual(cat['ok'], 1)
+ self.assertEqual(cat['error'], 1)
+
+ @mock.patch('dashboard.services.hardware.OrchClient.instance')
+ def test_string_status_format(self, mock_instance):
+ fake_client = mock.Mock()
+ fake_client.hardware.common = self._mock_common(
+ {'memory': MOCK_STRING_STATUS_DATA}
+ )
+ mock_instance.return_value = fake_client
+
+ result = HardwareService.get_summary(categories=['memory'])
+ cat = result['total']['category']['memory']
+ self.assertEqual(cat['ok'], 1)
+ self.assertEqual(cat['error'], 1)
+
+ @mock.patch('dashboard.services.hardware.OrchClient.instance')
+ def test_missing_status_treated_as_unknown(self, mock_instance):
+ fake_client = mock.Mock()
+ fake_client.hardware.common = self._mock_common(
+ {'memory': MOCK_MISSING_STATUS_DATA}
+ )
+ mock_instance.return_value = fake_client
+
+ result = HardwareService.get_summary(categories=['memory'])
+ cat = result['total']['category']['memory']
+ self.assertEqual(cat['ok'], 0)
+ self.assertEqual(cat['error'], 1)
+
+ @mock.patch('dashboard.services.hardware.OrchClient.instance')
+ def test_non_dict_component_treated_as_unknown(self, mock_instance):
+ fake_client = mock.Mock()
+ fake_client.hardware.common = self._mock_common(
+ {'memory': MOCK_NON_DICT_COMPONENT}
+ )
+ mock_instance.return_value = fake_client
+
+ result = HardwareService.get_summary(categories=['memory'])
+ cat = result['total']['category']['memory']
+ self.assertEqual(cat['ok'], 0)
+ self.assertEqual(cat['error'], 1)
+
+ @mock.patch('dashboard.services.hardware.OrchClient.instance')
+ def test_flawed_host_detected(self, mock_instance):
+ fake_client = mock.Mock()
+ fake_client.hardware.common = self._mock_common(MOCK_HARDWARE_DATA)
+ mock_instance.return_value = fake_client
+
+ result = HardwareService.get_summary(categories=['storage'])
+ self.assertEqual(result['host']['flawed'], 1)
+ self.assertTrue(result['host']['host1']['flawed'])
+
+ @mock.patch('dashboard.services.hardware.OrchClient.instance')
+ def test_healthy_host_not_flawed(self, mock_instance):
+ fake_client = mock.Mock()
+ fake_client.hardware.common = self._mock_common(MOCK_HARDWARE_DATA)
+ mock_instance.return_value = fake_client
+
+ result = HardwareService.get_summary(categories=['memory'])
+ self.assertEqual(result['host']['flawed'], 0)
+ self.assertFalse(result['host']['host1']['flawed'])
+ self.assertFalse(result['host']['host2']['flawed'])
+
+ @mock.patch('dashboard.services.hardware.OrchClient.instance')
+ def test_all_categories_totals(self, mock_instance):
+ fake_client = mock.Mock()
+ fake_client.hardware.common = self._mock_common(MOCK_HARDWARE_DATA)
+ mock_instance.return_value = fake_client
+
+ cats = ['memory', 'storage', 'processors', 'network', 'power', 'fans']
+ result = HardwareService.get_summary(categories=cats)
+
+ totals = result['total']['total']
+ self.assertEqual(totals['total'], 9)
+ self.assertEqual(totals['ok'], 8)
+ self.assertEqual(totals['error'], 1)
+
+ @mock.patch('dashboard.services.hardware.OrchClient.instance')
+ def test_empty_data_returns_zeros(self, mock_instance):
+ fake_client = mock.Mock()
+ fake_client.hardware.common = self._mock_common({'memory': {}})
+ mock_instance.return_value = fake_client
+
+ result = HardwareService.get_summary(categories=['memory'])
+ cat = result['total']['category']['memory']
+ self.assertEqual(cat['total'], 0)
+ self.assertEqual(cat['ok'], 0)
+ self.assertEqual(cat['error'], 0)