]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/blob
3603b2c15c192ac08de87599beecf4cfba4e762c
[ceph-ci.git] /
1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
3 import { ActivatedRoute } from '@angular/router';
4 import { BehaviorSubject, of, throwError } from 'rxjs';
5
6 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
7 import { RouterTestingModule } from '@angular/router/testing';
8
9 import { CephModule } from '~/app/ceph/ceph.module';
10 import { CephSharedModule } from '~/app/ceph/shared/ceph-shared.module';
11 import { CoreModule } from '~/app/core/core.module';
12 import { HostService } from '~/app/shared/api/host.service';
13 import { NvmeofService } from '~/app/shared/api/nvmeof.service';
14 import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
15 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
16 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
17 import { HostStatus } from '~/app/shared/enum/host-status.enum';
18 import { Permissions } from '~/app/shared/models/permissions';
19 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
20 import { SharedModule } from '~/app/shared/shared.module';
21 import { configureTestBed } from '~/testing/unit-test-helper';
22 import { TagModule } from 'carbon-components-angular';
23 import { NvmeofGatewayNodeComponent } from './nvmeof-gateway-node.component';
24
25 describe('NvmeofGatewayNodeComponent', () => {
26   let component: NvmeofGatewayNodeComponent;
27   let fixture: ComponentFixture<NvmeofGatewayNodeComponent>;
28   let hostService: HostService;
29   let orchService: OrchestratorService;
30   let nvmeofService: NvmeofService;
31
32   const fakeAuthStorageService = {
33     getPermissions: () => {
34       return new Permissions({ nvmeof: ['read', 'update', 'create', 'delete'] });
35     }
36   };
37
38   const mockGatewayNodes = [
39     {
40       hostname: 'gateway-node-1',
41       addr: '192.168.1.10',
42       status: HostStatus.AVAILABLE,
43       labels: ['nvmeof', 'gateway'],
44       services: [
45         {
46           type: 'nvmeof-gw',
47           id: 'gateway-1'
48         }
49       ],
50       ceph_version: 'ceph version 18.0.0',
51       sources: {
52         ceph: true,
53         orchestrator: true
54       },
55       service_instances: [] as any[]
56     },
57     {
58       hostname: 'gateway-node-2',
59       addr: '192.168.1.11',
60       status: HostStatus.MAINTENANCE,
61       labels: ['nvmeof'],
62       services: [
63         {
64           type: 'nvmeof-gw',
65           id: 'gateway-2'
66         }
67       ],
68       ceph_version: 'ceph version 18.0.0',
69       sources: {
70         ceph: true,
71         orchestrator: true
72       },
73       service_instances: [] as any[]
74     },
75     {
76       hostname: 'gateway-node-3',
77       addr: '192.168.1.12',
78       status: '',
79       labels: [],
80       services: [],
81       ceph_version: 'ceph version 18.0.0',
82       sources: {
83         ceph: true,
84         orchestrator: false
85       },
86       service_instances: [] as any[]
87     }
88   ];
89
90   configureTestBed({
91     imports: [
92       BrowserAnimationsModule,
93       CephSharedModule,
94       SharedModule,
95       HttpClientTestingModule,
96       RouterTestingModule,
97       CephModule,
98       CoreModule,
99       TagModule
100     ],
101     providers: [
102       { provide: AuthStorageService, useValue: fakeAuthStorageService },
103       {
104         provide: ActivatedRoute,
105         useValue: {
106           parent: {
107             params: new BehaviorSubject({ group: 'group1' }),
108             snapshot: {
109               params: { group: 'group1' }
110             }
111           },
112           snapshot: {
113             data: { mode: 'selector' }
114           }
115         }
116       }
117     ]
118   });
119
120   beforeEach(() => {
121     fixture = TestBed.createComponent(NvmeofGatewayNodeComponent);
122     component = fixture.componentInstance;
123     hostService = TestBed.inject(HostService);
124     orchService = TestBed.inject(OrchestratorService);
125     nvmeofService = TestBed.inject(NvmeofService);
126   });
127
128   it('should create', () => {
129     expect(component).toBeTruthy();
130   });
131
132   it('should initialize columns on component init', () => {
133     component.ngOnInit();
134
135     expect(component.columns).toBeDefined();
136     expect(component.columns.length).toBeGreaterThan(0);
137     expect(component.columns[0].name).toBe('Hostname');
138     expect(component.columns[0].prop).toBe('hostname');
139   });
140
141   it('should have all required columns defined', () => {
142     component.ngOnInit();
143
144     const columnProps = component.columns.map((col) => col.prop);
145     expect(columnProps).toContain('hostname');
146     expect(columnProps).toContain('addr');
147     expect(columnProps).toContain('status');
148     expect(columnProps).toContain('labels');
149   });
150
151   it('should initialize with default values', () => {
152     expect(component.hosts).toEqual([]);
153     expect(component.isLoadingHosts).toBe(false);
154     expect(component.count).toBe(0);
155     expect(component.permission).toBeDefined();
156   });
157
158   it('should update selection', () => {
159     const selection = new CdTableSelection();
160     selection.selected = [mockGatewayNodes[0]];
161
162     component.updateSelection(selection);
163
164     expect(component.selection).toBe(selection);
165     expect(component.selection.selected.length).toBe(1);
166   });
167
168   it('should get selected hosts', () => {
169     component.selection = new CdTableSelection();
170     component.selection.selected = [mockGatewayNodes[0], mockGatewayNodes[1]];
171
172     const selectedHosts = component.getSelectedHostnames();
173
174     expect(selectedHosts.length).toBe(2);
175     expect(selectedHosts[0]).toEqual(mockGatewayNodes[0].hostname);
176     expect(selectedHosts[1]).toEqual(mockGatewayNodes[1].hostname);
177   });
178
179   it('should get selected hostnames', () => {
180     component.selection = new CdTableSelection();
181     component.selection.selected = [mockGatewayNodes[0], mockGatewayNodes[1]];
182
183     const selectedHostnames = component.getSelectedHostnames();
184
185     expect(selectedHostnames).toEqual(['gateway-node-1', 'gateway-node-2']);
186   });
187
188   it('should load hosts with orchestrator available and facts feature enabled', fakeAsync(() => {
189     spyOn(nvmeofService, 'getAvailableHosts').and.returnValue(of([mockGatewayNodes[2]]));
190     component.getHosts(new CdTableFetchDataContext(() => undefined));
191
192     tick(100);
193     expect(nvmeofService.getAvailableHosts).toHaveBeenCalled();
194     expect(component.hosts.length).toBe(1);
195     expect(component.hosts[0]['hostname']).toBe('gateway-node-3');
196   }));
197
198   it('should set count to hosts length', fakeAsync(() => {
199     spyOn(nvmeofService, 'getAvailableHosts').and.returnValue(of(mockGatewayNodes));
200     component.getHosts(new CdTableFetchDataContext(() => undefined));
201
202     tick(100);
203     expect(component.count).toBe(component.hosts.length);
204   }));
205
206   it('should set count to 0 when no hosts are returned', fakeAsync(() => {
207     spyOn(nvmeofService, 'getAvailableHosts').and.returnValue(of([]));
208     component.getHosts(new CdTableFetchDataContext(() => undefined));
209
210     tick(100);
211     expect(component.count).toBe(0);
212     expect(component.hosts.length).toBe(0);
213   }));
214
215   it('should handle error when fetching hosts', fakeAsync(() => {
216     spyOn(nvmeofService, 'getAvailableHosts').and.returnValue(throwError(() => new Error('Error')));
217     const context = new CdTableFetchDataContext(() => undefined);
218     spyOn(context, 'error');
219
220     component.getHosts(context);
221
222     tick(100);
223     expect(component.isLoadingHosts).toBe(false);
224     expect(context.error).toHaveBeenCalled();
225   }));
226
227   it('should not re-fetch if already loading', fakeAsync(() => {
228     component.isLoadingHosts = true;
229     spyOn(nvmeofService, 'getAvailableHosts');
230
231     component.getHosts(new CdTableFetchDataContext(() => undefined));
232
233     tick(100);
234     expect(nvmeofService.getAvailableHosts).not.toHaveBeenCalled();
235   }));
236
237   it('should unsubscribe on component destroy', fakeAsync(() => {
238     spyOn(hostService, 'list').and.returnValue(of([]));
239     spyOn(orchService, 'status').and.returnValue(of({} as any));
240     spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
241     component.getHosts(new CdTableFetchDataContext(() => undefined));
242     tick(100);
243
244     const sub = component['sub'];
245     spyOn(sub, 'unsubscribe');
246
247     component.ngOnDestroy();
248
249     expect(sub.unsubscribe).toHaveBeenCalled();
250   }));
251
252   it('should handle host list with various label types', fakeAsync(() => {
253     const hostsWithLabels = [
254       {
255         ...mockGatewayNodes[0],
256         labels: ['nvmeof', 'gateway', 'high-priority']
257       },
258       {
259         ...mockGatewayNodes[2],
260         labels: []
261       }
262     ];
263
264     spyOn(hostService, 'list').and.returnValue(of(hostsWithLabels));
265     const mockOrcStatus: any = {
266       available: true,
267       features: new Map()
268     };
269
270     spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
271     spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(
272       of([
273         [
274           {
275             service_id: 'nvmeof.group1',
276             placement: { hosts: ['gateway-node-2'] }
277           }
278         ]
279       ] as any)
280     );
281     spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
282     component.groupName = 'group1';
283     fixture.detectChanges();
284
285     component.getHosts(new CdTableFetchDataContext(() => undefined));
286
287     tick(100);
288     expect(component.hosts[0]['labels'].length).toBe(3);
289     expect(component.hosts[1]['labels'].length).toBe(0);
290   }));
291
292   it('should handle hosts with multiple services', fakeAsync(() => {
293     const hostsWithServices = [
294       {
295         ...mockGatewayNodes[0],
296         services: [
297           { type: 'nvmeof-gw', id: 'gateway-1' },
298           { type: 'mon', id: '0' }
299         ]
300       }
301     ];
302
303     spyOn(hostService, 'list').and.returnValue(of(hostsWithServices));
304     const mockOrcStatus: any = {
305       available: true,
306       features: new Map()
307     };
308
309     spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
310     spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(
311       of([
312         [
313           {
314             service_id: 'nvmeof.group1',
315             placement: { hosts: ['gateway-node-2'] }
316           }
317         ]
318       ] as any)
319     );
320     spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
321     component.groupName = 'group1';
322     fixture.detectChanges();
323
324     component.getHosts(new CdTableFetchDataContext(() => undefined));
325
326     tick(100);
327     expect(component.hosts[0]['services'].length).toBe(2);
328   }));
329
330   it('should initialize table context on first getHosts call', fakeAsync(() => {
331     spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
332     const mockOrcStatus: any = {
333       available: true,
334       features: new Map()
335     };
336
337     spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
338     spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(
339       of([
340         [
341           {
342             service_id: 'nvmeof.group1',
343             placement: { hosts: ['gateway-node-1', 'gateway-node-2'] }
344           }
345         ]
346       ] as any)
347     );
348     spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
349     component.groupName = 'group1';
350     fixture.detectChanges();
351
352     expect((component as any).tableContext).toBeUndefined();
353
354     component.getHosts(new CdTableFetchDataContext(() => undefined));
355
356     tick(100);
357     expect((component as any).tableContext).toBeDefined();
358   }));
359
360   it('should reuse table context if already set', fakeAsync(() => {
361     const context = new CdTableFetchDataContext(() => undefined);
362     spyOn(hostService, 'list').and.returnValue(of(mockGatewayNodes));
363     const mockOrcStatus: any = {
364       available: true,
365       features: new Map()
366     };
367
368     spyOn(orchService, 'status').and.returnValue(of(mockOrcStatus));
369     spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[]]));
370     spyOn(hostService, 'checkHostsFactsAvailable').and.returnValue(true);
371     fixture.detectChanges();
372
373     component.getHosts(context);
374
375     tick(100);
376     const storedContext = (component as any).tableContext;
377     expect(storedContext).toBe(context);
378   }));
379
380   it('should fetch data using fetchHostsAndGroups in details mode', fakeAsync(() => {
381     (component as any).route.snapshot.data = { mode: 'details' };
382     component.ngOnInit();
383     component.groupName = 'group1';
384
385     spyOn(nvmeofService, 'fetchHostsAndGroups').and.returnValue(
386       of({
387         groups: [
388           [
389             {
390               service_id: 'nvmeof.group1',
391               spec: { group: 'group1' },
392               placement: { hosts: ['gateway-node-1'] }
393             }
394           ]
395         ],
396         hosts: mockGatewayNodes
397       } as any)
398     );
399
400     fixture.detectChanges();
401     component.getHosts(new CdTableFetchDataContext(() => undefined));
402     tick(100);
403
404     expect(nvmeofService.fetchHostsAndGroups).toHaveBeenCalled();
405     expect(component.hosts.length).toBe(1);
406     expect(component.hosts[0].hostname).toBe('gateway-node-1');
407   }));
408
409   it('should set selectionType to multiClick in selector mode', () => {
410     (component as any).route.snapshot.data = { mode: 'selector' };
411     component.ngOnInit();
412     expect(component.selectionType).toBe('multiClick');
413   });
414
415   it('should set selectionType to single in details mode', () => {
416     (component as any).route.snapshot.data = { mode: 'details' };
417     component.ngOnInit();
418     expect(component.selectionType).toBe('single');
419   });
420 });