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