1 import { DebugElement, Type } from '@angular/core';
2 import { ComponentFixture, TestBed } from '@angular/core/testing';
3 import { AbstractControl } from '@angular/forms';
4 import { By } from '@angular/platform-browser';
5 import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing';
7 import { NgbModal, NgbNav, NgbNavItem } from '@ng-bootstrap/ng-bootstrap';
8 import _ from 'lodash';
9 import { configureTestSuite } from 'ng-bullet';
10 import { of } from 'rxjs';
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';
28 AlertmanagerNotification,
29 AlertmanagerNotificationAlert,
31 } from '~/app/shared/models/prometheus-alerts';
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 }
42 TestBed.configureTestingModule(configuration);
47 export class PermissionHelper {
48 tac: TableActionsComponent;
49 permission: Permission;
50 selection: { single: object; multiple: object[] };
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
57 * Defaults to `{ single: {}, multiple: [{}, {}] }`.
59 constructor(permission: Permission, selection?: { single: object; multiple: object[] }) {
60 this.permission = permission;
61 this.selection = _.defaultTo(selection, { single: {}, multiple: [{}, {}] });
64 setPermissionsAndGetActions(tableActions: CdTableAction[]): any {
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;
73 this.tac = new TableActionsComponent();
74 this.tac.selection = new CdTableSelection();
75 this.tac.tableActions = [...tableActions];
76 this.tac.permission = this.permission;
89 const permissionText = perms.join(',');
91 result[permissionText !== '' ? permissionText : 'no-permissions'] = {
92 actions: this.tac.tableActions.map((action) => action.name),
93 primary: this.testScenarios()
103 const result: any = {};
104 // 'multiple selections'
105 result.multiple = this.testScenario(this.selection.multiple);
106 // 'select executing item'
107 result.executing = this.testScenario([
108 _.merge({ cdExecuting: 'someAction' }, this.selection.single)
110 // 'select non-executing item'
111 result.single = this.testScenario([this.selection.single]);
113 result.no = this.testScenario([]);
118 private testScenario(selection: object[]) {
119 this.setSelection(selection);
120 const action: CdTableAction = this.tac.currentAction;
121 return action ? action.name : '';
124 setSelection(selection: object[]) {
125 this.tac.selection.selected = selection;
126 this.tac.onSelectionChange();
130 export class FormHelper {
133 constructor(form: CdFormGroup) {
138 * Changes multiple values in multiple controls
140 setMultipleValues(values: { [controlName: string]: any }, markAsDirty?: boolean) {
141 Object.keys(values).forEach((key) => {
142 this.setValue(key, values[key], markAsDirty);
147 * Changes the value of a control
149 setValue(control: AbstractControl | string, value: any, markAsDirty?: boolean): AbstractControl {
150 control = this.getControl(control);
152 control.markAsDirty();
154 control.setValue(value);
158 private getControl(control: AbstractControl | string): AbstractControl {
159 if (typeof control === 'string') {
160 return this.form.get(control);
166 * Change the value of the control and expect the control to be valid afterwards.
168 expectValidChange(control: AbstractControl | string, value: any, markAsDirty?: boolean) {
169 this.expectValid(this.setValue(control, value, markAsDirty));
173 * Expect that the given control is valid.
175 expectValid(control: AbstractControl | string) {
176 // 'isValid' would be false for disabled controls
177 expect(this.getControl(control).errors).toBe(null);
181 * Change the value of the control and expect a specific error.
184 control: AbstractControl | string,
187 markAsDirty?: boolean
189 this.expectError(this.setValue(control, value, markAsDirty), error);
193 * Expect a specific error for the given control.
195 expectError(control: AbstractControl | string, error: string) {
196 expect(this.getControl(control).hasError(error)).toBeTruthy();
201 * Use this to mock 'modalService.open' to make the embedded component with it's fixture usable
202 * in tests. The function gives back all needed parts including the modal reference.
204 * Please make sure to call this function *inside* your mock and return the reference at the end.
206 export function modalServiceShow(componentClass: Type<any>, modalConfig: any) {
207 const modal: NgbModal = TestBed.inject(NgbModal);
208 const modalRef = modal.open(componentClass);
210 Object.assign(modalRef.componentInstance, modalConfig);
215 export class FixtureHelper {
216 fixture: ComponentFixture<any>;
218 constructor(fixture?: ComponentFixture<any>) {
220 this.updateFixture(fixture);
224 updateFixture(fixture: ComponentFixture<any>) {
225 this.fixture = fixture;
229 * Expect a list of id elements to be visible or not.
231 expectIdElementsVisible(ids: string[], visibility: boolean) {
232 ids.forEach((css) => {
233 this.expectElementVisible(`#${css}`, visibility);
238 * Expect a specific element to be visible or not.
240 expectElementVisible(css: string, visibility: boolean) {
241 expect(visibility).toBe(Boolean(this.getElementByCss(css)));
244 expectFormFieldToBe(css: string, value: string) {
245 const props = this.getElementByCss(css).properties;
246 expect(props['value'] || props['checked'].toString()).toBe(value);
249 expectTextToBe(css: string, value: string) {
250 expect(this.getText(css)).toBe(value);
253 clickElement(css: string) {
254 this.getElementByCss(css).triggerEventHandler('click', null);
255 this.fixture.detectChanges();
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();
265 getText(css: string) {
266 const e = this.getElementByCss(css);
267 return e ? e.nativeElement.textContent.trim() : null;
270 getTextAll(css: string) {
271 const elements = this.getElementByCssAll(css);
272 return elements.map((element) => {
273 return element ? element.nativeElement.textContent.trim() : null;
277 getElementByCss(css: string) {
278 this.fixture.detectChanges();
279 return this.fixture.debugElement.query(By.css(css));
282 getElementByCssAll(css: string) {
283 this.fixture.detectChanges();
284 return this.fixture.debugElement.queryAll(By.css(css));
288 export class PrometheusHelper {
289 createSilence(id: string) {
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(),
306 createRule(name: string, severity: string, alerts: any[]): PrometheusRule {
316 createAlert(name: string, state = 'active', timeMultiplier = 1): AlertmanagerAlert {
322 instance: 'someInstance',
324 severity: 'someSeverity'
327 description: `${name} is ${state}`
329 generatorURL: `http://${name}`,
330 startsAt: new Date(new Date('2022-02-22').getTime() * timeMultiplier).toString()
331 } as AlertmanagerAlert;
334 createNotificationAlert(name: string, status = 'firing'): AlertmanagerNotificationAlert {
341 description: `${name} is ${status}`
343 generatorURL: `http://${name}`
344 } as AlertmanagerNotificationAlert;
347 createNotification(alertNumber = 1, status = 'firing'): AlertmanagerNotification {
349 for (let i = 0; i < alertNumber; i++) {
350 alerts.push(this.createNotificationAlert('alert' + i, status));
352 return { alerts, status } as AlertmanagerNotification;
355 createLink(url: string) {
356 return `<a href="${url}" target="_blank"><i class="${Icons.lineChart}"></i></a>`;
360 export function expectItemTasks(item: any, executing: string, percentage?: number) {
362 executing = executing + '...';
364 executing = `${executing} ${percentage}%`;
367 expect(item.cdExecuting).toBe(executing);
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(
377 'thisUsernameIsWayyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyTooBig',
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');
390 export class RgwHelper {
391 static readonly daemons = RgwHelper.getDaemonList();
392 static readonly DAEMON_QUERY_PARAM = `daemon_name=${RgwHelper.daemons[0].id}`;
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);
406 static selectDaemon() {
407 const service = TestBed.inject(RgwDaemonService);
408 service.selectDaemon(this.daemons[0]);
419 device_class?: string
421 return { name, type, type_id, id, children, device_class };
424 static getPool = (name: string, id: number): Pool => {
425 return _.merge(new Pool(name), {
429 pg_placement_num: 256,
431 pg_placement_num_target: 256,
437 * Create the following test crush map:
440 * ----> 3x osd with ssd
443 * ------> 2x osd-rack with hdd
445 * ------> 2x osd-rack with ssd
447 static getCrushMap(): CrushNode[] {
450 this.getCrushNode('default', -1, 'root', 11, [-2, -3]),
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]),
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'),
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')
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.
473 * Host names follow the following naming convention:
475 * $index represents a number count started at 0 (like an index within an array) (same for OSDs)
477 * OSD names follow the following naming convention:
478 * osd.$hostIndex.$osdIndex
480 * The following crush map will be generated with the set defaults:
482 * --> host.0 (has only ssd OSDs)
487 * --> host.1 (has only hdd OSDs)
493 static generateSimpleCrushMap(hosts: number = 2, osds: number = 4): CrushNode[] {
495 const createOsdLeafs = (hostSuffix: number): number[] => {
498 const osdsInUse = hostSuffix * osds;
499 for (let o = 0; o < osds; o++) {
503 `osd.${hostSuffix}.${osdId}`,
508 hostSuffix % 2 === 0 ? 'ssd' : 'hdd'
515 const createHostBuckets = (): number[] => {
518 for (let h = 0; h < hosts; h++) {
519 const hostSuffix = hostId * -1 - 2;
520 hostIds.push(hostId);
522 this.getCrushNode(`host.${hostSuffix}`, hostId, 'host', 1, createOsdLeafs(hostSuffix))
528 nodes.push(this.getCrushNode('default', -1, 'root', 11, createHostBuckets()));
532 static getCrushRuleConfig(
535 failure_domain: string,
536 device_class?: string
546 static getCrushRule({
548 name = 'somePoolName',
550 failureDomain = 'osd',
551 itemName = 'default' // This string also sets the device type - "default~ssd" <- ssd usage only
556 failureDomain?: string;
559 const rule = new CrushRule();
560 rule.type = type === 'erasure' ? 3 : 1;
562 rule.rule_name = name;
581 static getInventoryDevice(
597 human_readable_size: '1 KB'
599 rejected_reasons: [''],
600 device_id: 'AAA-aaa-id0',
601 human_readable_type: 'nvme/ssd',
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);
613 static getNgbNavItems(fixture: ComponentFixture<any>) {
614 const debugElems = this.getNgbNavItemsDebugElems(fixture);
615 return debugElems.map((de) => de.injector.get(NgbNavItem));
618 static getTextContents(fixture: ComponentFixture<any>) {
619 const debugElems = this.getNgbNavItemsDebugElems(fixture);
620 return debugElems.map((de) => de.nativeElement.textContent);
623 private static getNgbNavItemsDebugElems(fixture: ComponentFixture<any>) {
624 const debugElem: DebugElement = fixture.debugElement;
625 return debugElem.queryAll(By.directive(NgbNavItem));
629 export class OrchestratorHelper {
631 * Mock Orchestrator status.
632 * @param available is the Orchestrator enabled?
633 * @param features A list of enabled Orchestrator features.
635 static mockStatus(available: boolean, features?: OrchestratorFeature[]) {
636 const orchStatus = { available: available, description: '', features: {} };
638 features.forEach((feature: OrchestratorFeature) => {
639 orchStatus.features[feature] = { available: true };
642 spyOn(TestBed.inject(OrchestratorService), 'status').and.callFake(() => of(orchStatus));
646 export class TableActionHelper {
648 * Verify table action buttons, including the button disabled state and disable description.
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.
655 static verifyTableActions = async (
656 fixture: ComponentFixture<any>,
657 tableActions: CdTableAction[],
659 [action: string]: { disabled: boolean; disableDesc: string };
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();
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)}`));
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
683 expect(actions).toEqual(expectResult);