]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/blob
da5ad197af65b242f082ed9948e868ea212189e1
[ceph-ci.git] /
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';
5
6 import { of, throwError } from 'rxjs';
7
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';
23
24 describe('NvmeofGatewayNodeComponent', () => {
25   let component: NvmeofGatewayNodeComponent;
26   let fixture: ComponentFixture<NvmeofGatewayNodeComponent>;
27   let hostService: HostService;
28   let orchService: OrchestratorService;
29   let nvmeofService: NvmeofService;
30
31   const fakeAuthStorageService = {
32     getPermissions: () => {
33       return new Permissions({ nvmeof: ['read', 'update', 'create', 'delete'] });
34     }
35   };
36
37   const mockGatewayNodes = [
38     {
39       hostname: 'gateway-node-1',
40       addr: '192.168.1.10',
41       status: HostStatus.AVAILABLE,
42       labels: ['nvmeof', 'gateway'],
43       services: [
44         {
45           type: 'nvmeof-gw',
46           id: 'gateway-1'
47         }
48       ],
49       ceph_version: 'ceph version 18.0.0',
50       sources: {
51         ceph: true,
52         orchestrator: true
53       },
54       service_instances: [] as any[]
55     },
56     {
57       hostname: 'gateway-node-2',
58       addr: '192.168.1.11',
59       status: HostStatus.MAINTENANCE,
60       labels: ['nvmeof'],
61       services: [
62         {
63           type: 'nvmeof-gw',
64           id: 'gateway-2'
65         }
66       ],
67       ceph_version: 'ceph version 18.0.0',
68       sources: {
69         ceph: true,
70         orchestrator: true
71       },
72       service_instances: [] as any[]
73     },
74     {
75       hostname: 'gateway-node-3',
76       addr: '192.168.1.12',
77       status: '',
78       labels: [],
79       services: [],
80       ceph_version: 'ceph version 18.0.0',
81       sources: {
82         ceph: true,
83         orchestrator: false
84       },
85       service_instances: [] as any[]
86     }
87   ];
88
89   configureTestBed({
90     imports: [
91       BrowserAnimationsModule,
92       CephSharedModule,
93       SharedModule,
94       HttpClientTestingModule,
95       RouterTestingModule,
96       CephModule,
97       CoreModule,
98       TagModule
99     ],
100     providers: [{ provide: AuthStorageService, useValue: fakeAuthStorageService }]
101   });
102
103   beforeEach(() => {
104     fixture = TestBed.createComponent(NvmeofGatewayNodeComponent);
105     component = fixture.componentInstance;
106     hostService = TestBed.inject(HostService);
107     orchService = TestBed.inject(OrchestratorService);
108     nvmeofService = TestBed.inject(NvmeofService);
109   });
110
111   it('should create', () => {
112     expect(component).toBeTruthy();
113   });
114
115   it('should initialize columns on component init', () => {
116     component.ngOnInit();
117
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');
122   });
123
124   it('should have all required columns defined', () => {
125     component.ngOnInit();
126
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');
132   });
133
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();
139   });
140
141   it('should update selection', () => {
142     const selection = new CdTableSelection();
143     selection.selected = [mockGatewayNodes[0]];
144
145     component.updateSelection(selection);
146
147     expect(component.selection).toBe(selection);
148     expect(component.selection.selected.length).toBe(1);
149   });
150
151   it('should get selected hosts', () => {
152     component.selection = new CdTableSelection();
153     component.selection.selected = [mockGatewayNodes[0], mockGatewayNodes[1]];
154
155     // ensure hosts list contains the selected hosts for lookup
156     component.hosts = [mockGatewayNodes[0], mockGatewayNodes[1]];
157
158     const selectedHosts = component
159       .getSelectedHostnames()
160       .map((hostname) => component.hosts.find((host) => host.hostname === hostname));
161
162     expect(selectedHosts.length).toBe(2);
163     expect(selectedHosts[0]).toEqual(mockGatewayNodes[0]);
164     expect(selectedHosts[1]).toEqual(mockGatewayNodes[1]);
165   });
166
167   it('should get selected hostnames', () => {
168     component.selection = new CdTableSelection();
169     component.selection.selected = [mockGatewayNodes[0], mockGatewayNodes[1]];
170
171     const selectedHostnames = component.getSelectedHostnames();
172
173     expect(selectedHostnames).toEqual(['gateway-node-1', 'gateway-node-2']);
174   });
175
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 = {
179       available: true,
180       features: new Map([['get_facts', { available: true }]])
181     };
182
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();
187
188     component.getHosts(new CdTableFetchDataContext(() => undefined));
189
190     setTimeout(() => {
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);
197       done();
198     }, 100);
199   });
200
201   it('should normalize empty status to "available"', (done) => {
202     spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
203     const mockOrcStatus: any = {
204       available: true,
205       features: new Map()
206     };
207
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();
212
213     component.getHosts(new CdTableFetchDataContext(() => undefined));
214
215     setTimeout(() => {
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);
219       done();
220     }, 100);
221   });
222
223   it('should set count to hosts length', (done) => {
224     spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
225     const mockOrcStatus: any = {
226       available: true,
227       features: new Map()
228     };
229
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();
234
235     component.getHosts(new CdTableFetchDataContext(() => undefined));
236
237     setTimeout(() => {
238       // Count should equal the filtered hosts length
239       expect(component.count).toBe(component.hosts.length);
240       done();
241     }, 100);
242   });
243
244   it('should set count to 0 when no hosts are returned', (done) => {
245     spyOn(hostService, 'list').and.returnValue(of([]));
246     const mockOrcStatus: any = {
247       available: true,
248       features: new Map()
249     };
250
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();
255
256     component.getHosts(new CdTableFetchDataContext(() => undefined));
257
258     setTimeout(() => {
259       expect(component.count).toBe(0);
260       expect(component.hosts.length).toBe(0);
261       done();
262     }, 100);
263   });
264
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 = {
269       available: true,
270       features: new Map()
271     };
272
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();
277
278     const context = new CdTableFetchDataContext(() => undefined);
279     spyOn(context, 'error');
280
281     component.getHosts(context);
282
283     setTimeout(() => {
284       expect(component.isLoadingHosts).toBe(false);
285       expect(context.error).toHaveBeenCalled();
286       done();
287     }, 100);
288   });
289
290   it('should check hosts facts available when orchestrator features present', () => {
291     component.orchStatus = {
292       available: true,
293       features: new Map([['get_facts', { available: true }]])
294     } as any;
295
296     spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
297
298     const result = component.checkHostsFactsAvailable();
299
300     expect(result).toBe(true);
301   });
302
303   it('should return false when get_facts feature is not available', () => {
304     component.orchStatus = {
305       available: true,
306       features: new Map([['other_feature', { available: true }]])
307     } as any;
308
309     const result = component.checkHostsFactsAvailable();
310
311     expect(result).toBe(false);
312   });
313
314   it('should return false when orchestrator status features are empty', () => {
315     component.orchStatus = {
316       available: true,
317       features: new Map()
318     } as any;
319
320     const result = component.checkHostsFactsAvailable();
321
322     expect(result).toBe(false);
323   });
324
325   it('should return false when orchestrator status is undefined', () => {
326     component.orchStatus = undefined;
327
328     const result = component.checkHostsFactsAvailable();
329
330     expect(result).toBe(false);
331   });
332
333   it('should not re-fetch if already loading', (done) => {
334     component.isLoadingHosts = true;
335     const hostListSpy = spyOn(hostService, 'list');
336
337     component.getHosts(new CdTableFetchDataContext(() => undefined));
338
339     setTimeout(() => {
340       expect(hostListSpy).not.toHaveBeenCalled();
341       done();
342     }, 100);
343   });
344
345   it('should unsubscribe on component destroy', () => {
346     const destroy$ = component['destroy$'];
347     spyOn(destroy$, 'next');
348     spyOn(destroy$, 'complete');
349
350     component.ngOnDestroy();
351
352     expect(destroy$.next).toHaveBeenCalled();
353     expect(destroy$.complete).toHaveBeenCalled();
354   });
355
356   it('should handle host list with various label types', (done) => {
357     const hostsWithLabels = [
358       {
359         ...mockGatewayNodes[0],
360         labels: ['nvmeof', 'gateway', 'high-priority']
361       },
362       {
363         ...mockGatewayNodes[2],
364         labels: []
365       }
366     ];
367
368     spyOn(hostService, 'list').and.returnValue(of(hostsWithLabels));
369     const mockOrcStatus: any = {
370       available: true,
371       features: new Map()
372     };
373
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();
378
379     component.getHosts(new CdTableFetchDataContext(() => undefined));
380
381     setTimeout(() => {
382       expect(component.hosts[0]['labels'].length).toBe(3);
383       expect(component.hosts[1]['labels'].length).toBe(0);
384       done();
385     }, 100);
386   });
387
388   it('should handle hosts with multiple services', (done) => {
389     const hostsWithServices = [
390       {
391         ...mockGatewayNodes[0],
392         services: [
393           { type: 'nvmeof-gw', id: 'gateway-1' },
394           { type: 'mon', id: '0' }
395         ]
396       }
397     ];
398
399     spyOn(hostService, 'list').and.returnValue(of(hostsWithServices));
400     const mockOrcStatus: any = {
401       available: true,
402       features: new Map()
403     };
404
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();
409
410     component.getHosts(new CdTableFetchDataContext(() => undefined));
411
412     setTimeout(() => {
413       expect(component.hosts[0]['services'].length).toBe(2);
414       done();
415     }, 100);
416   });
417
418   it('should initialize table context on first getHosts call', (done) => {
419     spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
420     const mockOrcStatus: any = {
421       available: true,
422       features: new Map()
423     };
424
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();
429
430     expect((component as any).tableContext).toBeNull();
431
432     component.getHosts(new CdTableFetchDataContext(() => undefined));
433
434     setTimeout(() => {
435       expect((component as any).tableContext).not.toBeNull();
436       done();
437     }, 100);
438   });
439
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 = {
444       available: true,
445       features: new Map()
446     };
447
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();
452
453     component.getHosts(context);
454
455     setTimeout(() => {
456       const storedContext = (component as any).tableContext;
457       expect(storedContext).toBe(context);
458       done();
459     }, 100);
460   });
461 });