]>
Commit | Line | Data |
---|---|---|
f67539c2 | 1 | import { DebugElement, Type } from '@angular/core'; |
f6b5b4d7 | 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; |
11fdf7f2 TL |
3 | import { AbstractControl } from '@angular/forms'; |
4 | import { By } from '@angular/platform-browser'; | |
f67539c2 | 5 | import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; |
11fdf7f2 | 6 | |
39ae355f | 7 | import { NgbModal, NgbNav, NgbNavItem, NgbNavLink } from '@ng-bootstrap/ng-bootstrap'; |
f67539c2 | 8 | import _ from 'lodash'; |
f67539c2 TL |
9 | import { of } from 'rxjs'; |
10 | ||
11 | import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model'; | |
12 | import { Pool } from '~/app/ceph/pool/pool'; | |
13 | import { RgwDaemon } from '~/app/ceph/rgw/models/rgw-daemon'; | |
14 | import { OrchestratorService } from '~/app/shared/api/orchestrator.service'; | |
15 | import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; | |
16 | import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component'; | |
17 | import { Icons } from '~/app/shared/enum/icons.enum'; | |
18 | import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; | |
19 | import { CdTableAction } from '~/app/shared/models/cd-table-action'; | |
20 | import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; | |
21 | import { CrushNode } from '~/app/shared/models/crush-node'; | |
22 | import { CrushRule, CrushRuleConfig } from '~/app/shared/models/crush-rule'; | |
23 | import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum'; | |
24 | import { Permission } from '~/app/shared/models/permissions'; | |
11fdf7f2 | 25 | import { |
494da23a TL |
26 | AlertmanagerAlert, |
27 | AlertmanagerNotification, | |
28 | AlertmanagerNotificationAlert, | |
29 | PrometheusRule | |
f67539c2 TL |
30 | } from '~/app/shared/models/prometheus-alerts'; |
31 | ||
32 | export function configureTestBed(configuration: any, entryComponents?: any) { | |
aee94f69 | 33 | beforeEach(async () => { |
f67539c2 TL |
34 | if (entryComponents) { |
35 | // Declare entryComponents without having to add them to a module | |
36 | // This is needed since Jest doesn't yet support not declaring entryComponents | |
aee94f69 TL |
37 | await TestBed.configureTestingModule(configuration).overrideModule( |
38 | BrowserDynamicTestingModule, | |
39 | { | |
40 | set: { entryComponents: entryComponents } | |
41 | } | |
42 | ); | |
f67539c2 | 43 | } else { |
aee94f69 | 44 | await TestBed.configureTestingModule(configuration); |
f67539c2 TL |
45 | } |
46 | }); | |
11fdf7f2 TL |
47 | } |
48 | ||
49 | export class PermissionHelper { | |
9f95a23c | 50 | tac: TableActionsComponent; |
11fdf7f2 | 51 | permission: Permission; |
f67539c2 | 52 | selection: { single: object; multiple: object[] }; |
11fdf7f2 | 53 | |
f67539c2 TL |
54 | /** |
55 | * @param permission The permissions used by this test. | |
56 | * @param selection The selection used by this test. Configure this if | |
57 | * the table actions require a more complex selection object to perform | |
58 | * a correct test run. | |
59 | * Defaults to `{ single: {}, multiple: [{}, {}] }`. | |
60 | */ | |
61 | constructor(permission: Permission, selection?: { single: object; multiple: object[] }) { | |
11fdf7f2 | 62 | this.permission = permission; |
f67539c2 | 63 | this.selection = _.defaultTo(selection, { single: {}, multiple: [{}, {}] }); |
11fdf7f2 TL |
64 | } |
65 | ||
9f95a23c TL |
66 | setPermissionsAndGetActions(tableActions: CdTableAction[]): any { |
67 | const result = {}; | |
68 | [true, false].forEach((create) => { | |
69 | [true, false].forEach((update) => { | |
70 | [true, false].forEach((deleteP) => { | |
71 | this.permission.create = create; | |
72 | this.permission.update = update; | |
73 | this.permission.delete = deleteP; | |
74 | ||
75 | this.tac = new TableActionsComponent(); | |
76 | this.tac.selection = new CdTableSelection(); | |
77 | this.tac.tableActions = [...tableActions]; | |
78 | this.tac.permission = this.permission; | |
79 | this.tac.ngOnInit(); | |
80 | ||
81 | const perms = []; | |
82 | if (create) { | |
83 | perms.push('create'); | |
84 | } | |
85 | if (update) { | |
86 | perms.push('update'); | |
87 | } | |
88 | if (deleteP) { | |
89 | perms.push('delete'); | |
90 | } | |
91 | const permissionText = perms.join(','); | |
92 | ||
93 | result[permissionText !== '' ? permissionText : 'no-permissions'] = { | |
94 | actions: this.tac.tableActions.map((action) => action.name), | |
95 | primary: this.testScenarios() | |
96 | }; | |
97 | }); | |
98 | }); | |
99 | }); | |
100 | ||
101 | return result; | |
102 | } | |
103 | ||
104 | testScenarios() { | |
105 | const result: any = {}; | |
106 | // 'multiple selections' | |
f67539c2 | 107 | result.multiple = this.testScenario(this.selection.multiple); |
9f95a23c | 108 | // 'select executing item' |
f67539c2 TL |
109 | result.executing = this.testScenario([ |
110 | _.merge({ cdExecuting: 'someAction' }, this.selection.single) | |
111 | ]); | |
9f95a23c | 112 | // 'select non-executing item' |
f67539c2 | 113 | result.single = this.testScenario([this.selection.single]); |
9f95a23c TL |
114 | // 'no selection' |
115 | result.no = this.testScenario([]); | |
116 | ||
117 | return result; | |
118 | } | |
119 | ||
120 | private testScenario(selection: object[]) { | |
11fdf7f2 | 121 | this.setSelection(selection); |
f67539c2 TL |
122 | const action: CdTableAction = this.tac.currentAction; |
123 | return action ? action.name : ''; | |
11fdf7f2 TL |
124 | } |
125 | ||
126 | setSelection(selection: object[]) { | |
9f95a23c | 127 | this.tac.selection.selected = selection; |
f67539c2 | 128 | this.tac.onSelectionChange(); |
11fdf7f2 TL |
129 | } |
130 | } | |
131 | ||
132 | export class FormHelper { | |
133 | form: CdFormGroup; | |
134 | ||
135 | constructor(form: CdFormGroup) { | |
136 | this.form = form; | |
137 | } | |
138 | ||
139 | /** | |
140 | * Changes multiple values in multiple controls | |
141 | */ | |
142 | setMultipleValues(values: { [controlName: string]: any }, markAsDirty?: boolean) { | |
143 | Object.keys(values).forEach((key) => { | |
144 | this.setValue(key, values[key], markAsDirty); | |
145 | }); | |
146 | } | |
147 | ||
148 | /** | |
149 | * Changes the value of a control | |
150 | */ | |
151 | setValue(control: AbstractControl | string, value: any, markAsDirty?: boolean): AbstractControl { | |
152 | control = this.getControl(control); | |
153 | if (markAsDirty) { | |
154 | control.markAsDirty(); | |
155 | } | |
156 | control.setValue(value); | |
157 | return control; | |
158 | } | |
159 | ||
160 | private getControl(control: AbstractControl | string): AbstractControl { | |
161 | if (typeof control === 'string') { | |
162 | return this.form.get(control); | |
163 | } | |
164 | return control; | |
165 | } | |
166 | ||
167 | /** | |
168 | * Change the value of the control and expect the control to be valid afterwards. | |
169 | */ | |
170 | expectValidChange(control: AbstractControl | string, value: any, markAsDirty?: boolean) { | |
171 | this.expectValid(this.setValue(control, value, markAsDirty)); | |
172 | } | |
173 | ||
174 | /** | |
175 | * Expect that the given control is valid. | |
176 | */ | |
177 | expectValid(control: AbstractControl | string) { | |
178 | // 'isValid' would be false for disabled controls | |
179 | expect(this.getControl(control).errors).toBe(null); | |
180 | } | |
181 | ||
182 | /** | |
183 | * Change the value of the control and expect a specific error. | |
184 | */ | |
185 | expectErrorChange( | |
186 | control: AbstractControl | string, | |
187 | value: any, | |
188 | error: string, | |
189 | markAsDirty?: boolean | |
190 | ) { | |
191 | this.expectError(this.setValue(control, value, markAsDirty), error); | |
192 | } | |
193 | ||
194 | /** | |
195 | * Expect a specific error for the given control. | |
196 | */ | |
197 | expectError(control: AbstractControl | string, error: string) { | |
198 | expect(this.getControl(control).hasError(error)).toBeTruthy(); | |
199 | } | |
200 | } | |
201 | ||
9f95a23c | 202 | /** |
f67539c2 | 203 | * Use this to mock 'modalService.open' to make the embedded component with it's fixture usable |
9f95a23c TL |
204 | * in tests. The function gives back all needed parts including the modal reference. |
205 | * | |
206 | * Please make sure to call this function *inside* your mock and return the reference at the end. | |
207 | */ | |
208 | export function modalServiceShow(componentClass: Type<any>, modalConfig: any) { | |
f67539c2 TL |
209 | const modal: NgbModal = TestBed.inject(NgbModal); |
210 | const modalRef = modal.open(componentClass); | |
211 | if (modalConfig) { | |
212 | Object.assign(modalRef.componentInstance, modalConfig); | |
213 | } | |
214 | return modalRef; | |
9f95a23c TL |
215 | } |
216 | ||
11fdf7f2 TL |
217 | export class FixtureHelper { |
218 | fixture: ComponentFixture<any>; | |
219 | ||
9f95a23c TL |
220 | constructor(fixture?: ComponentFixture<any>) { |
221 | if (fixture) { | |
222 | this.updateFixture(fixture); | |
223 | } | |
224 | } | |
225 | ||
226 | updateFixture(fixture: ComponentFixture<any>) { | |
11fdf7f2 TL |
227 | this.fixture = fixture; |
228 | } | |
229 | ||
230 | /** | |
231 | * Expect a list of id elements to be visible or not. | |
232 | */ | |
233 | expectIdElementsVisible(ids: string[], visibility: boolean) { | |
234 | ids.forEach((css) => { | |
235 | this.expectElementVisible(`#${css}`, visibility); | |
236 | }); | |
237 | } | |
238 | ||
239 | /** | |
240 | * Expect a specific element to be visible or not. | |
241 | */ | |
242 | expectElementVisible(css: string, visibility: boolean) { | |
9f95a23c | 243 | expect(visibility).toBe(Boolean(this.getElementByCss(css))); |
11fdf7f2 TL |
244 | } |
245 | ||
246 | expectFormFieldToBe(css: string, value: string) { | |
247 | const props = this.getElementByCss(css).properties; | |
248 | expect(props['value'] || props['checked'].toString()).toBe(value); | |
249 | } | |
250 | ||
9f95a23c TL |
251 | expectTextToBe(css: string, value: string) { |
252 | expect(this.getText(css)).toBe(value); | |
253 | } | |
254 | ||
11fdf7f2 TL |
255 | clickElement(css: string) { |
256 | this.getElementByCss(css).triggerEventHandler('click', null); | |
257 | this.fixture.detectChanges(); | |
258 | } | |
259 | ||
9f95a23c TL |
260 | selectElement(css: string, value: string) { |
261 | const nativeElement = this.getElementByCss(css).nativeElement; | |
262 | nativeElement.value = value; | |
263 | nativeElement.dispatchEvent(new Event('change')); | |
264 | this.fixture.detectChanges(); | |
265 | } | |
266 | ||
11fdf7f2 TL |
267 | getText(css: string) { |
268 | const e = this.getElementByCss(css); | |
269 | return e ? e.nativeElement.textContent.trim() : null; | |
270 | } | |
271 | ||
9f95a23c TL |
272 | getTextAll(css: string) { |
273 | const elements = this.getElementByCssAll(css); | |
274 | return elements.map((element) => { | |
275 | return element ? element.nativeElement.textContent.trim() : null; | |
276 | }); | |
277 | } | |
278 | ||
11fdf7f2 TL |
279 | getElementByCss(css: string) { |
280 | this.fixture.detectChanges(); | |
281 | return this.fixture.debugElement.query(By.css(css)); | |
282 | } | |
9f95a23c TL |
283 | |
284 | getElementByCssAll(css: string) { | |
285 | this.fixture.detectChanges(); | |
286 | return this.fixture.debugElement.queryAll(By.css(css)); | |
287 | } | |
11fdf7f2 TL |
288 | } |
289 | ||
290 | export class PrometheusHelper { | |
9f95a23c | 291 | createSilence(id: string) { |
494da23a TL |
292 | return { |
293 | id: id, | |
294 | createdBy: `Creator of ${id}`, | |
295 | comment: `A comment for ${id}`, | |
296 | startsAt: new Date('2022-02-22T22:22:00').toISOString(), | |
297 | endsAt: new Date('2022-02-23T22:22:00').toISOString(), | |
298 | matchers: [ | |
299 | { | |
300 | name: 'job', | |
301 | value: 'someJob', | |
302 | isRegex: true | |
303 | } | |
304 | ] | |
305 | }; | |
306 | } | |
307 | ||
9f95a23c | 308 | createRule(name: string, severity: string, alerts: any[]): PrometheusRule { |
494da23a TL |
309 | return { |
310 | name: name, | |
311 | labels: { | |
312 | severity: severity | |
313 | }, | |
314 | alerts: alerts | |
315 | } as PrometheusRule; | |
316 | } | |
317 | ||
9f95a23c | 318 | createAlert(name: string, state = 'active', timeMultiplier = 1): AlertmanagerAlert { |
11fdf7f2 TL |
319 | return { |
320 | fingerprint: name, | |
321 | status: { state }, | |
322 | labels: { | |
494da23a TL |
323 | alertname: name, |
324 | instance: 'someInstance', | |
325 | job: 'someJob', | |
326 | severity: 'someSeverity' | |
11fdf7f2 TL |
327 | }, |
328 | annotations: { | |
f67539c2 | 329 | description: `${name} is ${state}` |
11fdf7f2 TL |
330 | }, |
331 | generatorURL: `http://${name}`, | |
332 | startsAt: new Date(new Date('2022-02-22').getTime() * timeMultiplier).toString() | |
494da23a | 333 | } as AlertmanagerAlert; |
11fdf7f2 TL |
334 | } |
335 | ||
9f95a23c | 336 | createNotificationAlert(name: string, status = 'firing'): AlertmanagerNotificationAlert { |
11fdf7f2 TL |
337 | return { |
338 | status: status, | |
339 | labels: { | |
340 | alertname: name | |
341 | }, | |
342 | annotations: { | |
f67539c2 | 343 | description: `${name} is ${status}` |
11fdf7f2 TL |
344 | }, |
345 | generatorURL: `http://${name}` | |
494da23a | 346 | } as AlertmanagerNotificationAlert; |
11fdf7f2 TL |
347 | } |
348 | ||
494da23a | 349 | createNotification(alertNumber = 1, status = 'firing'): AlertmanagerNotification { |
11fdf7f2 TL |
350 | const alerts = []; |
351 | for (let i = 0; i < alertNumber; i++) { | |
352 | alerts.push(this.createNotificationAlert('alert' + i, status)); | |
353 | } | |
494da23a | 354 | return { alerts, status } as AlertmanagerNotification; |
11fdf7f2 TL |
355 | } |
356 | ||
9f95a23c TL |
357 | createLink(url: string) { |
358 | return `<a href="${url}" target="_blank"><i class="${Icons.lineChart}"></i></a>`; | |
11fdf7f2 TL |
359 | } |
360 | } | |
361 | ||
eafe8130 TL |
362 | export function expectItemTasks(item: any, executing: string, percentage?: number) { |
363 | if (executing) { | |
364 | executing = executing + '...'; | |
365 | if (percentage) { | |
366 | executing = `${executing} ${percentage}%`; | |
367 | } | |
368 | } | |
369 | expect(item.cdExecuting).toBe(executing); | |
370 | } | |
1911f103 TL |
371 | |
372 | export class IscsiHelper { | |
373 | static validateUser(formHelper: FormHelper, fieldName: string) { | |
374 | formHelper.expectErrorChange(fieldName, 'short', 'pattern'); | |
375 | formHelper.expectValidChange(fieldName, 'thisIsCorrect'); | |
376 | formHelper.expectErrorChange(fieldName, '##?badChars?##', 'pattern'); | |
377 | formHelper.expectErrorChange( | |
378 | fieldName, | |
379 | 'thisUsernameIsWayyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyTooBig', | |
380 | 'pattern' | |
381 | ); | |
382 | } | |
383 | ||
384 | static validatePassword(formHelper: FormHelper, fieldName: string) { | |
385 | formHelper.expectErrorChange(fieldName, 'short', 'pattern'); | |
386 | formHelper.expectValidChange(fieldName, 'thisIsCorrect'); | |
387 | formHelper.expectErrorChange(fieldName, '##?badChars?##', 'pattern'); | |
388 | formHelper.expectErrorChange(fieldName, 'thisPasswordIsWayTooBig', 'pattern'); | |
389 | } | |
390 | } | |
f6b5b4d7 | 391 | |
f67539c2 TL |
392 | export class RgwHelper { |
393 | static readonly daemons = RgwHelper.getDaemonList(); | |
394 | static readonly DAEMON_QUERY_PARAM = `daemon_name=${RgwHelper.daemons[0].id}`; | |
395 | ||
396 | static getDaemonList() { | |
397 | const daemonList: RgwDaemon[] = []; | |
398 | for (let daemonIndex = 1; daemonIndex <= 3; daemonIndex++) { | |
399 | const rgwDaemon = new RgwDaemon(); | |
400 | rgwDaemon.id = `daemon${daemonIndex}`; | |
401 | rgwDaemon.default = daemonIndex === 2; | |
402 | rgwDaemon.zonegroup_name = `zonegroup${daemonIndex}`; | |
403 | daemonList.push(rgwDaemon); | |
404 | } | |
405 | return daemonList; | |
406 | } | |
407 | ||
408 | static selectDaemon() { | |
409 | const service = TestBed.inject(RgwDaemonService); | |
410 | service.selectDaemon(this.daemons[0]); | |
411 | } | |
412 | } | |
413 | ||
f6b5b4d7 TL |
414 | export class Mocks { |
415 | static getCrushNode( | |
416 | name: string, | |
417 | id: number, | |
418 | type: string, | |
419 | type_id: number, | |
420 | children?: number[], | |
421 | device_class?: string | |
422 | ): CrushNode { | |
423 | return { name, type, type_id, id, children, device_class }; | |
424 | } | |
425 | ||
f67539c2 TL |
426 | static getPool = (name: string, id: number): Pool => { |
427 | return _.merge(new Pool(name), { | |
428 | pool: id, | |
429 | type: 'replicated', | |
430 | pg_num: 256, | |
431 | pg_placement_num: 256, | |
432 | pg_num_target: 256, | |
433 | pg_placement_num_target: 256, | |
434 | size: 3 | |
435 | }); | |
436 | }; | |
437 | ||
f6b5b4d7 TL |
438 | /** |
439 | * Create the following test crush map: | |
440 | * > default | |
441 | * --> ssd-host | |
442 | * ----> 3x osd with ssd | |
443 | * --> mix-host | |
444 | * ----> hdd-rack | |
445 | * ------> 2x osd-rack with hdd | |
446 | * ----> ssd-rack | |
447 | * ------> 2x osd-rack with ssd | |
448 | */ | |
449 | static getCrushMap(): CrushNode[] { | |
450 | return [ | |
451 | // Root node | |
452 | this.getCrushNode('default', -1, 'root', 11, [-2, -3]), | |
453 | // SSD host | |
454 | this.getCrushNode('ssd-host', -2, 'host', 1, [1, 0, 2]), | |
455 | this.getCrushNode('osd.0', 0, 'osd', 0, undefined, 'ssd'), | |
456 | this.getCrushNode('osd.1', 1, 'osd', 0, undefined, 'ssd'), | |
457 | this.getCrushNode('osd.2', 2, 'osd', 0, undefined, 'ssd'), | |
458 | // SSD and HDD mixed devices host | |
459 | this.getCrushNode('mix-host', -3, 'host', 1, [-4, -5]), | |
460 | // HDD rack | |
461 | this.getCrushNode('hdd-rack', -4, 'rack', 3, [3, 4]), | |
462 | this.getCrushNode('osd2.0', 3, 'osd-rack', 0, undefined, 'hdd'), | |
463 | this.getCrushNode('osd2.1', 4, 'osd-rack', 0, undefined, 'hdd'), | |
464 | // SSD rack | |
465 | this.getCrushNode('ssd-rack', -5, 'rack', 3, [5, 6]), | |
466 | this.getCrushNode('osd3.0', 5, 'osd-rack', 0, undefined, 'ssd'), | |
467 | this.getCrushNode('osd3.1', 6, 'osd-rack', 0, undefined, 'ssd') | |
468 | ]; | |
469 | } | |
470 | ||
471 | /** | |
472 | * Generates an simple crush map with multiple hosts that have OSDs with either ssd or hdd OSDs. | |
473 | * Hosts with zero or even numbers at the end have SSD OSDs the other hosts have hdd OSDs. | |
474 | * | |
475 | * Host names follow the following naming convention: | |
476 | * host.$index | |
477 | * $index represents a number count started at 0 (like an index within an array) (same for OSDs) | |
478 | * | |
479 | * OSD names follow the following naming convention: | |
480 | * osd.$hostIndex.$osdIndex | |
481 | * | |
482 | * The following crush map will be generated with the set defaults: | |
483 | * > default | |
484 | * --> host.0 (has only ssd OSDs) | |
485 | * ----> osd.0.0 | |
486 | * ----> osd.0.1 | |
487 | * ----> osd.0.2 | |
488 | * ----> osd.0.3 | |
489 | * --> host.1 (has only hdd OSDs) | |
490 | * ----> osd.1.0 | |
491 | * ----> osd.1.1 | |
492 | * ----> osd.1.2 | |
493 | * ----> osd.1.3 | |
494 | */ | |
495 | static generateSimpleCrushMap(hosts: number = 2, osds: number = 4): CrushNode[] { | |
496 | const nodes = []; | |
497 | const createOsdLeafs = (hostSuffix: number): number[] => { | |
498 | let osdId = 0; | |
499 | const osdIds = []; | |
500 | const osdsInUse = hostSuffix * osds; | |
501 | for (let o = 0; o < osds; o++) { | |
502 | osdIds.push(osdId); | |
503 | nodes.push( | |
504 | this.getCrushNode( | |
505 | `osd.${hostSuffix}.${osdId}`, | |
506 | osdId + osdsInUse, | |
507 | 'osd', | |
508 | 0, | |
509 | undefined, | |
510 | hostSuffix % 2 === 0 ? 'ssd' : 'hdd' | |
511 | ) | |
512 | ); | |
513 | osdId++; | |
514 | } | |
515 | return osdIds; | |
516 | }; | |
517 | const createHostBuckets = (): number[] => { | |
518 | let hostId = -2; | |
519 | const hostIds = []; | |
520 | for (let h = 0; h < hosts; h++) { | |
521 | const hostSuffix = hostId * -1 - 2; | |
522 | hostIds.push(hostId); | |
523 | nodes.push( | |
524 | this.getCrushNode(`host.${hostSuffix}`, hostId, 'host', 1, createOsdLeafs(hostSuffix)) | |
525 | ); | |
526 | hostId--; | |
527 | } | |
528 | return hostIds; | |
529 | }; | |
530 | nodes.push(this.getCrushNode('default', -1, 'root', 11, createHostBuckets())); | |
531 | return nodes; | |
532 | } | |
533 | ||
534 | static getCrushRuleConfig( | |
535 | name: string, | |
536 | root: string, | |
537 | failure_domain: string, | |
538 | device_class?: string | |
539 | ): CrushRuleConfig { | |
540 | return { | |
541 | name, | |
542 | root, | |
543 | failure_domain, | |
544 | device_class | |
545 | }; | |
546 | } | |
547 | ||
548 | static getCrushRule({ | |
549 | id = 0, | |
550 | name = 'somePoolName', | |
f6b5b4d7 TL |
551 | type = 'replicated', |
552 | failureDomain = 'osd', | |
553 | itemName = 'default' // This string also sets the device type - "default~ssd" <- ssd usage only | |
554 | }: { | |
f6b5b4d7 TL |
555 | id?: number; |
556 | name?: string; | |
557 | type?: string; | |
558 | failureDomain?: string; | |
559 | itemName?: string; | |
560 | }): CrushRule { | |
f6b5b4d7 | 561 | const rule = new CrushRule(); |
20effc67 | 562 | rule.type = type === 'erasure' ? 3 : 1; |
f6b5b4d7 | 563 | rule.rule_id = id; |
f6b5b4d7 TL |
564 | rule.rule_name = name; |
565 | rule.steps = [ | |
566 | { | |
567 | item_name: itemName, | |
568 | item: -1, | |
569 | op: 'take' | |
570 | }, | |
571 | { | |
572 | num: 0, | |
573 | type: failureDomain, | |
574 | op: 'choose_firstn' | |
575 | }, | |
576 | { | |
577 | op: 'emit' | |
578 | } | |
579 | ]; | |
580 | return rule; | |
581 | } | |
f67539c2 TL |
582 | |
583 | static getInventoryDevice( | |
584 | hostname: string, | |
585 | uid: string, | |
586 | path = 'sda', | |
587 | available = false | |
588 | ): InventoryDevice { | |
589 | return { | |
590 | hostname, | |
591 | uid, | |
592 | path, | |
593 | available, | |
594 | sys_api: { | |
595 | vendor: 'AAA', | |
596 | model: 'aaa', | |
597 | size: 1024, | |
598 | rotational: 'false', | |
599 | human_readable_size: '1 KB' | |
600 | }, | |
601 | rejected_reasons: [''], | |
602 | device_id: 'AAA-aaa-id0', | |
603 | human_readable_type: 'nvme/ssd', | |
604 | osd_ids: [] | |
605 | }; | |
606 | } | |
607 | } | |
608 | ||
609 | export class TabHelper { | |
610 | static getNgbNav(fixture: ComponentFixture<any>) { | |
611 | const debugElem: DebugElement = fixture.debugElement; | |
612 | return debugElem.query(By.directive(NgbNav)).injector.get(NgbNav); | |
613 | } | |
614 | ||
615 | static getNgbNavItems(fixture: ComponentFixture<any>) { | |
616 | const debugElems = this.getNgbNavItemsDebugElems(fixture); | |
617 | return debugElems.map((de) => de.injector.get(NgbNavItem)); | |
618 | } | |
619 | ||
620 | static getTextContents(fixture: ComponentFixture<any>) { | |
621 | const debugElems = this.getNgbNavItemsDebugElems(fixture); | |
622 | return debugElems.map((de) => de.nativeElement.textContent); | |
623 | } | |
624 | ||
625 | private static getNgbNavItemsDebugElems(fixture: ComponentFixture<any>) { | |
626 | const debugElem: DebugElement = fixture.debugElement; | |
39ae355f | 627 | return debugElem.queryAll(By.directive(NgbNavLink)); |
f67539c2 TL |
628 | } |
629 | } | |
630 | ||
631 | export class OrchestratorHelper { | |
632 | /** | |
633 | * Mock Orchestrator status. | |
634 | * @param available is the Orchestrator enabled? | |
635 | * @param features A list of enabled Orchestrator features. | |
636 | */ | |
637 | static mockStatus(available: boolean, features?: OrchestratorFeature[]) { | |
638 | const orchStatus = { available: available, description: '', features: {} }; | |
639 | if (features) { | |
640 | features.forEach((feature: OrchestratorFeature) => { | |
641 | orchStatus.features[feature] = { available: true }; | |
642 | }); | |
643 | } | |
644 | spyOn(TestBed.inject(OrchestratorService), 'status').and.callFake(() => of(orchStatus)); | |
645 | } | |
646 | } | |
647 | ||
648 | export class TableActionHelper { | |
649 | /** | |
650 | * Verify table action buttons, including the button disabled state and disable description. | |
651 | * | |
652 | * @param fixture test fixture | |
653 | * @param tableActions table actions | |
654 | * @param expectResult expected values. e.g. {Create: { disabled: true, disableDesc: 'not supported'}}. | |
655 | * Expect the Create button to be disabled with 'not supported' tooltip. | |
656 | */ | |
657 | static verifyTableActions = async ( | |
658 | fixture: ComponentFixture<any>, | |
659 | tableActions: CdTableAction[], | |
660 | expectResult: { | |
661 | [action: string]: { disabled: boolean; disableDesc: string }; | |
662 | } | |
663 | ) => { | |
664 | // click dropdown to update all actions buttons | |
665 | const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle')); | |
666 | dropDownToggle.triggerEventHandler('click', null); | |
667 | fixture.detectChanges(); | |
668 | await fixture.whenStable(); | |
669 | ||
670 | const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent)); | |
671 | const toClassName = TestBed.inject(TableActionsComponent).toClassName; | |
672 | const getActionElement = (action: CdTableAction) => | |
673 | tableActionElement.query(By.css(`[ngbDropdownItem].${toClassName(action)}`)); | |
674 | ||
675 | const actions = {}; | |
676 | tableActions.forEach((action) => { | |
677 | const actionElement = getActionElement(action); | |
678 | if (expectResult[action.name]) { | |
679 | actions[action.name] = { | |
39ae355f | 680 | disabled: actionElement.classes.disabled ? true : false, |
f67539c2 TL |
681 | disableDesc: actionElement.properties.title |
682 | }; | |
683 | } | |
684 | }); | |
685 | expect(actions).toEqual(expectResult); | |
686 | }; | |
f6b5b4d7 | 687 | } |