1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, TestBed } from '@angular/core/testing';
3 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
4 import { RouterTestingModule } from '@angular/router/testing';
6 import { of, throwError } from 'rxjs';
8 import { CephModule } from '~/app/ceph/ceph.module';
9 import { CephSharedModule } from '~/app/ceph/shared/ceph-shared.module';
10 import { CoreModule } from '~/app/core/core.module';
11 import { HostService } from '~/app/shared/api/host.service';
12 import { NvmeofService } from '~/app/shared/api/nvmeof.service';
13 import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
14 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
15 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
16 import { HostStatus } from '~/app/shared/enum/host-status.enum';
17 import { Permissions } from '~/app/shared/models/permissions';
18 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
19 import { SharedModule } from '~/app/shared/shared.module';
20 import { configureTestBed } from '~/testing/unit-test-helper';
21 import { TagModule } from 'carbon-components-angular';
22 import { NvmeofGatewayNodeComponent } from './nvmeof-gateway-node.component';
24 describe('NvmeofGatewayNodeComponent', () => {
25 let component: NvmeofGatewayNodeComponent;
26 let fixture: ComponentFixture<NvmeofGatewayNodeComponent>;
27 let hostService: HostService;
28 let orchService: OrchestratorService;
29 let nvmeofService: NvmeofService;
31 const fakeAuthStorageService = {
32 getPermissions: () => {
33 return new Permissions({ nvmeof: ['read', 'update', 'create', 'delete'] });
37 const mockGatewayNodes = [
39 hostname: 'gateway-node-1',
41 status: HostStatus.AVAILABLE,
42 labels: ['nvmeof', 'gateway'],
49 ceph_version: 'ceph version 18.0.0',
54 service_instances: [] as any[]
57 hostname: 'gateway-node-2',
59 status: HostStatus.MAINTENANCE,
67 ceph_version: 'ceph version 18.0.0',
72 service_instances: [] as any[]
75 hostname: 'gateway-node-3',
80 ceph_version: 'ceph version 18.0.0',
85 service_instances: [] as any[]
91 BrowserAnimationsModule,
94 HttpClientTestingModule,
100 providers: [{ provide: AuthStorageService, useValue: fakeAuthStorageService }]
104 fixture = TestBed.createComponent(NvmeofGatewayNodeComponent);
105 component = fixture.componentInstance;
106 hostService = TestBed.inject(HostService);
107 orchService = TestBed.inject(OrchestratorService);
108 nvmeofService = TestBed.inject(NvmeofService);
111 it('should create', () => {
112 expect(component).toBeTruthy();
115 it('should initialize columns on component init', () => {
116 component.ngOnInit();
118 expect(component.columns).toBeDefined();
119 expect(component.columns.length).toBeGreaterThan(0);
120 expect(component.columns[0].name).toBe('Hostname');
121 expect(component.columns[0].prop).toBe('hostname');
124 it('should have all required columns defined', () => {
125 component.ngOnInit();
127 const columnProps = component.columns.map((col) => col.prop);
128 expect(columnProps).toContain('hostname');
129 expect(columnProps).toContain('addr');
130 expect(columnProps).toContain('status');
131 expect(columnProps).toContain('labels');
134 it('should initialize with default values', () => {
135 expect(component.hosts).toEqual([]);
136 expect(component.isLoadingHosts).toBe(false);
137 expect(component.count).toBe(5);
138 expect(component.permission).toBeDefined();
141 it('should update selection', () => {
142 const selection = new CdTableSelection();
143 selection.selected = [mockGatewayNodes[0]];
145 component.updateSelection(selection);
147 expect(component.selection).toBe(selection);
148 expect(component.selection.selected.length).toBe(1);
151 it('should get selected hosts', () => {
152 component.selection = new CdTableSelection();
153 component.selection.selected = [mockGatewayNodes[0], mockGatewayNodes[1]];
155 // ensure hosts list contains the selected hosts for lookup
156 component.hosts = [mockGatewayNodes[0], mockGatewayNodes[1]];
158 const selectedHosts = component
159 .getSelectedHostnames()
160 .map((hostname) => component.hosts.find((host) => host.hostname === hostname));
162 expect(selectedHosts.length).toBe(2);
163 expect(selectedHosts[0]).toEqual(mockGatewayNodes[0]);
164 expect(selectedHosts[1]).toEqual(mockGatewayNodes[1]);
167 it('should get selected hostnames', () => {
168 component.selection = new CdTableSelection();
169 component.selection.selected = [mockGatewayNodes[0], mockGatewayNodes[1]];
171 const selectedHostnames = component.getSelectedHostnames();
173 expect(selectedHostnames).toEqual(['gateway-node-1', 'gateway-node-2']);
176 it('should load hosts with orchestrator available and facts feature enabled', (done) => {
177 const hostListSpy = spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
178 const mockOrcStatus: any = {
180 features: new Map([['get_facts', { available: true }]])
183 spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
184 spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
185 spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
186 fixture.detectChanges();
188 component.getHosts(new CdTableFetchDataContext(() => undefined));
191 expect(hostListSpy).toHaveBeenCalled();
192 // Only hosts with status 'available', '' or 'running' are included (excluding 'maintenance')
193 expect(component.hosts.length).toBe(2);
194 expect(component.isLoadingHosts).toBe(false);
195 expect(component.hosts[0]['hostname']).toBe('gateway-node-1');
196 expect(component.hosts[0]['status']).toBe(HostStatus.AVAILABLE);
201 it('should normalize empty status to "available"', (done) => {
202 spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
203 const mockOrcStatus: any = {
208 spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
209 spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
210 spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
211 fixture.detectChanges();
213 component.getHosts(new CdTableFetchDataContext(() => undefined));
216 // Host at index 1 in filtered list (gateway-node-3 has empty status which becomes 'available')
217 const nodeWithEmptyStatus = component.hosts.find((h) => h.hostname === 'gateway-node-3');
218 expect(nodeWithEmptyStatus?.['status']).toBe(HostStatus.AVAILABLE);
223 it('should set count to hosts length', (done) => {
224 spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
225 const mockOrcStatus: any = {
230 spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
231 spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
232 spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
233 fixture.detectChanges();
235 component.getHosts(new CdTableFetchDataContext(() => undefined));
238 // Count should equal the filtered hosts length
239 expect(component.count).toBe(component.hosts.length);
244 it('should set count to 0 when no hosts are returned', (done) => {
245 spyOn(hostService, 'list').and.returnValue(of([]));
246 const mockOrcStatus: any = {
251 spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
252 spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
253 spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
254 fixture.detectChanges();
256 component.getHosts(new CdTableFetchDataContext(() => undefined));
259 expect(component.count).toBe(0);
260 expect(component.hosts.length).toBe(0);
265 it('should handle error when fetching hosts', (done) => {
266 const errorMsg = 'Failed to fetch hosts';
267 spyOn(hostService, 'list').and.returnValue(throwError(() => new Error(errorMsg)));
268 const mockOrcStatus: any = {
273 spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
274 spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
275 spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
276 fixture.detectChanges();
278 const context = new CdTableFetchDataContext(() => undefined);
279 spyOn(context, 'error');
281 component.getHosts(context);
284 expect(component.isLoadingHosts).toBe(false);
285 expect(context.error).toHaveBeenCalled();
290 it('should check hosts facts available when orchestrator features present', () => {
291 component.orchStatus = {
293 features: new Map([['get_facts', { available: true }]])
296 spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
298 const result = component.checkHostsFactsAvailable();
300 expect(result).toBe(true);
303 it('should return false when get_facts feature is not available', () => {
304 component.orchStatus = {
306 features: new Map([['other_feature', { available: true }]])
309 const result = component.checkHostsFactsAvailable();
311 expect(result).toBe(false);
314 it('should return false when orchestrator status features are empty', () => {
315 component.orchStatus = {
320 const result = component.checkHostsFactsAvailable();
322 expect(result).toBe(false);
325 it('should return false when orchestrator status is undefined', () => {
326 component.orchStatus = undefined;
328 const result = component.checkHostsFactsAvailable();
330 expect(result).toBe(false);
333 it('should not re-fetch if already loading', (done) => {
334 component.isLoadingHosts = true;
335 const hostListSpy = spyOn(hostService, 'list');
337 component.getHosts(new CdTableFetchDataContext(() => undefined));
340 expect(hostListSpy).not.toHaveBeenCalled();
345 it('should unsubscribe on component destroy', () => {
346 const destroy$ = component['destroy$'];
347 spyOn(destroy$, 'next');
348 spyOn(destroy$, 'complete');
350 component.ngOnDestroy();
352 expect(destroy$.next).toHaveBeenCalled();
353 expect(destroy$.complete).toHaveBeenCalled();
356 it('should handle host list with various label types', (done) => {
357 const hostsWithLabels = [
359 ...mockGatewayNodes[0],
360 labels: ['nvmeof', 'gateway', 'high-priority']
363 ...mockGatewayNodes[2],
368 spyOn(hostService, 'list').and.returnValue(of(hostsWithLabels));
369 const mockOrcStatus: any = {
374 spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
375 spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
376 spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
377 fixture.detectChanges();
379 component.getHosts(new CdTableFetchDataContext(() => undefined));
382 expect(component.hosts[0]['labels'].length).toBe(3);
383 expect(component.hosts[1]['labels'].length).toBe(0);
388 it('should handle hosts with multiple services', (done) => {
389 const hostsWithServices = [
391 ...mockGatewayNodes[0],
393 { type: 'nvmeof-gw', id: 'gateway-1' },
394 { type: 'mon', id: '0' }
399 spyOn(hostService, 'list').and.returnValue(of(hostsWithServices));
400 const mockOrcStatus: any = {
405 spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
406 spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
407 spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
408 fixture.detectChanges();
410 component.getHosts(new CdTableFetchDataContext(() => undefined));
413 expect(component.hosts[0]['services'].length).toBe(2);
418 it('should initialize table context on first getHosts call', (done) => {
419 spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
420 const mockOrcStatus: any = {
425 spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
426 spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
427 spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
428 fixture.detectChanges();
430 expect((component as any).tableContext).toBeNull();
432 component.getHosts(new CdTableFetchDataContext(() => undefined));
435 expect((component as any).tableContext).not.toBeNull();
440 it('should reuse table context if already set', (done) => {
441 const context = new CdTableFetchDataContext(() => undefined);
442 spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
443 const mockOrcStatus: any = {
448 spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
449 spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
450 spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
451 fixture.detectChanges();
453 component.getHosts(context);
456 const storedContext = (component as any).tableContext;
457 expect(storedContext).toBe(context);