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, NgbNavLink } from '@ng-bootstrap/ng-bootstrap';
8 import _ from 'lodash';
9 import { of } from 'rxjs';
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';
27 AlertmanagerNotification,
28 AlertmanagerNotificationAlert,
30 } from '~/app/shared/models/prometheus-alerts';
32 export function configureTestBed(configuration: any, entryComponents?: any) {
33 beforeEach(async () => {
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
37 await TestBed.configureTestingModule(configuration).overrideModule(
38 BrowserDynamicTestingModule,
40 set: { entryComponents: entryComponents }
44 await TestBed.configureTestingModule(configuration);
49 export class PermissionHelper {
50 tac: TableActionsComponent;
51 permission: Permission;
52 selection: { single: object; multiple: object[] };
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
59 * Defaults to `{ single: {}, multiple: [{}, {}] }`.
61 constructor(permission: Permission, selection?: { single: object; multiple: object[] }) {
62 this.permission = permission;
63 this.selection = _.defaultTo(selection, { single: {}, multiple: [{}, {}] });
66 setPermissionsAndGetActions(tableActions: CdTableAction[]): any {
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;
75 this.tac = new TableActionsComponent();
76 this.tac.selection = new CdTableSelection();
77 this.tac.tableActions = [...tableActions];
78 this.tac.permission = this.permission;
91 const permissionText = perms.join(',');
93 result[permissionText !== '' ? permissionText : 'no-permissions'] = {
94 actions: this.tac.tableActions.map((action) => action.name),
95 primary: this.testScenarios()
105 const result: any = {};
106 // 'multiple selections'
107 result.multiple = this.testScenario(this.selection.multiple);
108 // 'select executing item'
109 result.executing = this.testScenario([
110 _.merge({ cdExecuting: 'someAction' }, this.selection.single)
112 // 'select non-executing item'
113 result.single = this.testScenario([this.selection.single]);
115 result.no = this.testScenario([]);
120 private testScenario(selection: object[]) {
121 this.setSelection(selection);
122 const action: CdTableAction = this.tac.currentAction;
123 return action ? action.name : '';
126 setSelection(selection: object[]) {
127 this.tac.selection.selected = selection;
128 this.tac.onSelectionChange();
132 export class FormHelper {
135 constructor(form: CdFormGroup) {
140 * Changes multiple values in multiple controls
142 setMultipleValues(values: { [controlName: string]: any }, markAsDirty?: boolean) {
143 Object.keys(values).forEach((key) => {
144 this.setValue(key, values[key], markAsDirty);
149 * Changes the value of a control
151 setValue(control: AbstractControl | string, value: any, markAsDirty?: boolean): AbstractControl {
152 control = this.getControl(control);
154 control.markAsDirty();
156 control.setValue(value);
160 private getControl(control: AbstractControl | string): AbstractControl {
161 if (typeof control === 'string') {
162 return this.form.get(control);
168 * Change the value of the control and expect the control to be valid afterwards.
170 expectValidChange(control: AbstractControl | string, value: any, markAsDirty?: boolean) {
171 this.expectValid(this.setValue(control, value, markAsDirty));
175 * Expect that the given control is valid.
177 expectValid(control: AbstractControl | string) {
178 // 'isValid' would be false for disabled controls
179 expect(this.getControl(control).errors).toBe(null);
183 * Change the value of the control and expect a specific error.
186 control: AbstractControl | string,
189 markAsDirty?: boolean
191 this.expectError(this.setValue(control, value, markAsDirty), error);
195 * Expect a specific error for the given control.
197 expectError(control: AbstractControl | string, error: string) {
198 expect(this.getControl(control).hasError(error)).toBeTruthy();
203 * Use this to mock 'modalService.open' to make the embedded component with it's fixture usable
204 * in tests. The function gives back all needed parts including the modal reference.
206 * Please make sure to call this function *inside* your mock and return the reference at the end.
208 export function modalServiceShow(componentClass: Type<any>, modalConfig: any) {
209 const modal: NgbModal = TestBed.inject(NgbModal);
210 const modalRef = modal.open(componentClass);
212 Object.assign(modalRef.componentInstance, modalConfig);
217 export class FixtureHelper {
218 fixture: ComponentFixture<any>;
220 constructor(fixture?: ComponentFixture<any>) {
222 this.updateFixture(fixture);
226 updateFixture(fixture: ComponentFixture<any>) {
227 this.fixture = fixture;
231 * Expect a list of id elements to be visible or not.
233 expectIdElementsVisible(ids: string[], visibility: boolean) {
234 ids.forEach((css) => {
235 this.expectElementVisible(`#${css}`, visibility);
240 * Expect a specific element to be visible or not.
242 expectElementVisible(css: string, visibility: boolean) {
243 expect(visibility).toBe(Boolean(this.getElementByCss(css)));
246 expectFormFieldToBe(css: string, value: string) {
247 const props = this.getElementByCss(css).properties;
248 expect(props['value'] || props['checked'].toString()).toBe(value);
251 expectTextToBe(css: string, value: string) {
252 expect(this.getText(css)).toBe(value);
255 clickElement(css: string) {
256 this.getElementByCss(css).triggerEventHandler('click', null);
257 this.fixture.detectChanges();
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();
267 getText(css: string) {
268 const e = this.getElementByCss(css);
269 return e ? e.nativeElement.textContent.trim() : null;
272 getTextAll(css: string) {
273 const elements = this.getElementByCssAll(css);
274 return elements.map((element) => {
275 return element ? element.nativeElement.textContent.trim() : null;
279 getElementByCss(css: string) {
280 this.fixture.detectChanges();
281 return this.fixture.debugElement.query(By.css(css));
284 getElementByCssAll(css: string) {
285 this.fixture.detectChanges();
286 return this.fixture.debugElement.queryAll(By.css(css));
290 export class PrometheusHelper {
291 createSilence(id: string) {
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(),
308 createRule(name: string, severity: string, alerts: any[]): PrometheusRule {
318 createAlert(name: string, state = 'active', timeMultiplier = 1): AlertmanagerAlert {
324 instance: 'someInstance',
326 severity: 'someSeverity'
329 description: `${name} is ${state}`
331 generatorURL: `http://${name}`,
332 startsAt: new Date(new Date('2022-02-22').getTime() * timeMultiplier).toString()
333 } as AlertmanagerAlert;
336 createNotificationAlert(name: string, status = 'firing'): AlertmanagerNotificationAlert {
343 description: `${name} is ${status}`
345 generatorURL: `http://${name}`
346 } as AlertmanagerNotificationAlert;
349 createNotification(alertNumber = 1, status = 'firing'): AlertmanagerNotification {
351 for (let i = 0; i < alertNumber; i++) {
352 alerts.push(this.createNotificationAlert('alert' + i, status));
354 return { alerts, status } as AlertmanagerNotification;
357 createLink(url: string) {
358 return `<a href="${url}" target="_blank"><i class="${Icons.lineChart}"></i></a>`;
362 export function expectItemTasks(item: any, executing: string, percentage?: number) {
364 executing = executing + '...';
366 executing = `${executing} ${percentage}%`;
369 expect(item.cdExecuting).toBe(executing);
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(
379 'thisUsernameIsWayyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyTooBig',
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');
392 export class RgwHelper {
393 static readonly daemons = RgwHelper.getDaemonList();
394 static readonly DAEMON_QUERY_PARAM = `daemon_name=${RgwHelper.daemons[0].id}`;
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);
408 static selectDaemon() {
409 const service = TestBed.inject(RgwDaemonService);
410 service.selectDaemon(this.daemons[0]);
421 device_class?: string
423 return { name, type, type_id, id, children, device_class };
426 static getPool = (name: string, id: number): Pool => {
427 return _.merge(new Pool(name), {
431 pg_placement_num: 256,
433 pg_placement_num_target: 256,
439 * Create the following test crush map:
442 * ----> 3x osd with ssd
445 * ------> 2x osd-rack with hdd
447 * ------> 2x osd-rack with ssd
449 static getCrushMap(): CrushNode[] {
452 this.getCrushNode('default', -1, 'root', 11, [-2, -3]),
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]),
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'),
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')
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.
475 * Host names follow the following naming convention:
477 * $index represents a number count started at 0 (like an index within an array) (same for OSDs)
479 * OSD names follow the following naming convention:
480 * osd.$hostIndex.$osdIndex
482 * The following crush map will be generated with the set defaults:
484 * --> host.0 (has only ssd OSDs)
489 * --> host.1 (has only hdd OSDs)
495 static generateSimpleCrushMap(hosts: number = 2, osds: number = 4): CrushNode[] {
497 const createOsdLeafs = (hostSuffix: number): number[] => {
500 const osdsInUse = hostSuffix * osds;
501 for (let o = 0; o < osds; o++) {
505 `osd.${hostSuffix}.${osdId}`,
510 hostSuffix % 2 === 0 ? 'ssd' : 'hdd'
517 const createHostBuckets = (): number[] => {
520 for (let h = 0; h < hosts; h++) {
521 const hostSuffix = hostId * -1 - 2;
522 hostIds.push(hostId);
524 this.getCrushNode(`host.${hostSuffix}`, hostId, 'host', 1, createOsdLeafs(hostSuffix))
530 nodes.push(this.getCrushNode('default', -1, 'root', 11, createHostBuckets()));
534 static getCrushRuleConfig(
537 failure_domain: string,
538 device_class?: string
548 static getCrushRule({
550 name = 'somePoolName',
552 failureDomain = 'osd',
553 itemName = 'default' // This string also sets the device type - "default~ssd" <- ssd usage only
558 failureDomain?: string;
561 const rule = new CrushRule();
562 rule.type = type === 'erasure' ? 3 : 1;
564 rule.rule_name = name;
583 static getInventoryDevice(
599 human_readable_size: '1 KB'
601 rejected_reasons: [''],
602 device_id: 'AAA-aaa-id0',
603 human_readable_type: 'nvme/ssd',
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);
615 static getNgbNavItems(fixture: ComponentFixture<any>) {
616 const debugElems = this.getNgbNavItemsDebugElems(fixture);
617 return debugElems.map((de) => de.injector.get(NgbNavItem));
620 static getTextContents(fixture: ComponentFixture<any>) {
621 const debugElems = this.getNgbNavItemsDebugElems(fixture);
622 return debugElems.map((de) => de.nativeElement.textContent);
625 private static getNgbNavItemsDebugElems(fixture: ComponentFixture<any>) {
626 const debugElem: DebugElement = fixture.debugElement;
627 return debugElem.queryAll(By.directive(NgbNavLink));
631 export class OrchestratorHelper {
633 * Mock Orchestrator status.
634 * @param available is the Orchestrator enabled?
635 * @param features A list of enabled Orchestrator features.
637 static mockStatus(available: boolean, features?: OrchestratorFeature[]) {
638 const orchStatus = { available: available, description: '', features: {} };
640 features.forEach((feature: OrchestratorFeature) => {
641 orchStatus.features[feature] = { available: true };
644 spyOn(TestBed.inject(OrchestratorService), 'status').and.callFake(() => of(orchStatus));
648 export class TableActionHelper {
650 * Verify table action buttons, including the button disabled state and disable description.
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.
657 static verifyTableActions = async (
658 fixture: ComponentFixture<any>,
659 tableActions: CdTableAction[],
661 [action: string]: { disabled: boolean; disableDesc: string };
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();
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)}`));
676 tableActions.forEach((action) => {
677 const actionElement = getActionElement(action);
678 if (expectResult[action.name]) {
679 actions[action.name] = {
680 disabled: actionElement.classes.disabled ? true : false,
681 disableDesc: actionElement.properties.title
685 expect(actions).toEqual(expectResult);