summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts')
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts598
1 files changed, 598 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts
new file mode 100644
index 000000000..418983150
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts
@@ -0,0 +1,598 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { ActivatedRoute, Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import moment from 'moment';
+import { ToastrModule } from 'ngx-toastr';
+import { of, throwError } from 'rxjs';
+
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { ErrorComponent } from '~/app/core/error/error.component';
+import { PrometheusService } from '~/app/shared/api/prometheus.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { AlertmanagerSilence } from '~/app/shared/models/alertmanager-silence';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import {
+ configureTestBed,
+ FixtureHelper,
+ FormHelper,
+ PrometheusHelper
+} from '~/testing/unit-test-helper';
+import { SilenceFormComponent } from './silence-form.component';
+
+describe('SilenceFormComponent', () => {
+ // SilenceFormComponent specific
+ let component: SilenceFormComponent;
+ let fixture: ComponentFixture<SilenceFormComponent>;
+ let form: CdFormGroup;
+ // Spied on
+ let prometheusService: PrometheusService;
+ let authStorageService: AuthStorageService;
+ let notificationService: NotificationService;
+ let router: Router;
+ // Spies
+ let rulesSpy: jasmine.Spy;
+ let ifPrometheusSpy: jasmine.Spy;
+ // Helper
+ let prometheus: PrometheusHelper;
+ let formHelper: FormHelper;
+ let fixtureH: FixtureHelper;
+ let params: Record<string, any>;
+ // Date mocking related
+ const baseTime = '2022-02-22 00:00';
+ const beginningDate = '2022-02-22T00:00:12.35';
+ let prometheusPermissions: Permission;
+
+ const routes: Routes = [{ path: '404', component: ErrorComponent }];
+ configureTestBed({
+ declarations: [ErrorComponent, SilenceFormComponent],
+ imports: [
+ HttpClientTestingModule,
+ RouterTestingModule.withRoutes(routes),
+ SharedModule,
+ ToastrModule.forRoot(),
+ NgbTooltipModule,
+ NgbPopoverModule,
+ ReactiveFormsModule
+ ],
+ providers: [
+ {
+ provide: ActivatedRoute,
+ useValue: { params: { subscribe: (fn: Function) => fn(params) } }
+ }
+ ]
+ });
+
+ const createMatcher = (name: string, value: any, isRegex: boolean) => ({ name, value, isRegex });
+
+ const addMatcher = (name: string, value: any, isRegex: boolean) =>
+ component['setMatcher'](createMatcher(name, value, isRegex));
+
+ const callInit = () =>
+ fixture.ngZone.run(() => {
+ component['init']();
+ });
+
+ const changeAction = (action: string) => {
+ const modes = {
+ add: '/monitoring/silences/add',
+ alertAdd: '/monitoring/silences/add/alert0',
+ recreate: '/monitoring/silences/recreate/someExpiredId',
+ edit: '/monitoring/silences/edit/someNotExpiredId'
+ };
+ Object.defineProperty(router, 'url', { value: modes[action] });
+ callInit();
+ };
+
+ beforeEach(() => {
+ params = {};
+ spyOn(Date, 'now').and.returnValue(new Date(beginningDate));
+
+ prometheus = new PrometheusHelper();
+ prometheusService = TestBed.inject(PrometheusService);
+ spyOn(prometheusService, 'getAlerts').and.callFake(() => {
+ const name = _.split(router.url, '/').pop();
+ return of([prometheus.createAlert(name)]);
+ });
+ ifPrometheusSpy = spyOn(prometheusService, 'ifPrometheusConfigured').and.callFake((fn) => fn());
+ rulesSpy = spyOn(prometheusService, 'getRules').and.callFake(() =>
+ of({
+ groups: [
+ {
+ file: '',
+ interval: 0,
+ name: '',
+ rules: [
+ prometheus.createRule('alert0', 'someSeverity', [prometheus.createAlert('alert0')]),
+ prometheus.createRule('alert1', 'someSeverity', []),
+ prometheus.createRule('alert2', 'someOtherSeverity', [
+ prometheus.createAlert('alert2')
+ ])
+ ]
+ }
+ ]
+ })
+ );
+
+ router = TestBed.inject(Router);
+
+ notificationService = TestBed.inject(NotificationService);
+ spyOn(notificationService, 'show').and.stub();
+
+ authStorageService = TestBed.inject(AuthStorageService);
+ spyOn(authStorageService, 'getUsername').and.returnValue('someUser');
+
+ spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
+ prometheus: prometheusPermissions
+ }));
+ prometheusPermissions = new Permission(['update', 'delete', 'read', 'create']);
+ fixture = TestBed.createComponent(SilenceFormComponent);
+ fixtureH = new FixtureHelper(fixture);
+ component = fixture.componentInstance;
+ form = component.form;
+ formHelper = new FormHelper(form);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(_.isArray(component.rules)).toBeTruthy();
+ });
+
+ it('should have set the logged in user name as creator', () => {
+ expect(component.form.getValue('createdBy')).toBe('someUser');
+ });
+
+ it('should call disablePrometheusConfig on error calling getRules', () => {
+ spyOn(prometheusService, 'disablePrometheusConfig');
+ rulesSpy.and.callFake(() => throwError({}));
+ callInit();
+ expect(component.rules).toEqual([]);
+ expect(prometheusService.disablePrometheusConfig).toHaveBeenCalled();
+ });
+
+ it('should remind user if prometheus is not set when it is not configured', () => {
+ ifPrometheusSpy.and.callFake((_x: any, fn: Function) => fn());
+ callInit();
+ expect(component.rules).toEqual([]);
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.info,
+ 'Please add your Prometheus host to the dashboard configuration and refresh the page',
+ undefined,
+ undefined,
+ 'Prometheus'
+ );
+ });
+
+ describe('throw error for not allowed users', () => {
+ let navigateSpy: jasmine.Spy;
+
+ const expectError = (action: string, redirected: boolean) => {
+ Object.defineProperty(router, 'url', { value: action });
+ if (redirected) {
+ expect(() => callInit()).toThrowError(DashboardNotFoundError);
+ } else {
+ expect(() => callInit()).not.toThrowError();
+ }
+ navigateSpy.calls.reset();
+ };
+
+ beforeEach(() => {
+ navigateSpy = spyOn(router, 'navigate').and.stub();
+ });
+
+ it('should throw error if not allowed', () => {
+ prometheusPermissions = new Permission(['delete', 'read']);
+ expectError('add', true);
+ expectError('alertAdd', true);
+ });
+
+ it('should throw error if user does not have minimum permissions to create silences', () => {
+ prometheusPermissions = new Permission(['update', 'delete', 'read']);
+ expectError('add', true);
+ prometheusPermissions = new Permission(['update', 'delete', 'create']);
+ expectError('recreate', true);
+ });
+
+ it('should throw error if user does not have minimum permissions to update silences', () => {
+ prometheusPermissions = new Permission(['delete', 'read']);
+ expectError('edit', true);
+ prometheusPermissions = new Permission(['create', 'delete', 'update']);
+ expectError('edit', true);
+ });
+
+ it('does not throw error if user has minimum permissions to create silences', () => {
+ prometheusPermissions = new Permission(['create', 'read']);
+ expectError('add', false);
+ expectError('alertAdd', false);
+ expectError('recreate', false);
+ });
+
+ it('does not throw error if user has minimum permissions to update silences', () => {
+ prometheusPermissions = new Permission(['read', 'create']);
+ expectError('edit', false);
+ });
+ });
+
+ describe('choose the right action', () => {
+ const expectMode = (routerMode: string, edit: boolean, recreate: boolean, action: string) => {
+ changeAction(routerMode);
+ expect(component.recreate).toBe(recreate);
+ expect(component.edit).toBe(edit);
+ expect(component.action).toBe(action);
+ };
+
+ beforeEach(() => {
+ spyOn(prometheusService, 'getSilences').and.callFake(() => {
+ const id = _.split(router.url, '/').pop();
+ return of([prometheus.createSilence(id)]);
+ });
+ });
+
+ it('should have no special action activate by default', () => {
+ expectMode('add', false, false, 'Create');
+ expect(prometheusService.getSilences).not.toHaveBeenCalled();
+ expect(component.form.value).toEqual({
+ comment: null,
+ createdBy: 'someUser',
+ duration: '2h',
+ startsAt: baseTime,
+ endsAt: '2022-02-22 02:00'
+ });
+ });
+
+ it('should be in edit action if route includes edit', () => {
+ params = { id: 'someNotExpiredId' };
+ expectMode('edit', true, false, 'Edit');
+ expect(prometheusService.getSilences).toHaveBeenCalled();
+ expect(component.form.value).toEqual({
+ comment: `A comment for ${params.id}`,
+ createdBy: `Creator of ${params.id}`,
+ duration: '1d',
+ startsAt: '2022-02-22 22:22',
+ endsAt: '2022-02-23 22:22'
+ });
+ expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
+ });
+
+ it('should be in recreation action if route includes recreate', () => {
+ params = { id: 'someExpiredId' };
+ expectMode('recreate', false, true, 'Recreate');
+ expect(prometheusService.getSilences).toHaveBeenCalled();
+ expect(component.form.value).toEqual({
+ comment: `A comment for ${params.id}`,
+ createdBy: `Creator of ${params.id}`,
+ duration: '2h',
+ startsAt: baseTime,
+ endsAt: '2022-02-22 02:00'
+ });
+ expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
+ });
+
+ it('adds matchers based on the label object of the alert with the given id', () => {
+ params = { id: 'alert0' };
+ expectMode('alertAdd', false, false, 'Create');
+ expect(prometheusService.getSilences).not.toHaveBeenCalled();
+ expect(prometheusService.getAlerts).toHaveBeenCalled();
+ expect(component.matchers).toEqual([
+ createMatcher('alertname', 'alert0', false),
+ createMatcher('instance', 'someInstance', false),
+ createMatcher('job', 'someJob', false),
+ createMatcher('severity', 'someSeverity', false)
+ ]);
+ expect(component.matcherMatch).toEqual({
+ cssClass: 'has-success',
+ status: 'Matches 1 rule with 1 active alert.'
+ });
+ });
+ });
+
+ describe('time', () => {
+ const changeEndDate = (text: string) => component.form.patchValue({ endsAt: text });
+ const changeStartDate = (text: string) => component.form.patchValue({ startsAt: text });
+
+ it('have all dates set at beginning', () => {
+ expect(form.getValue('startsAt')).toEqual(baseTime);
+ expect(form.getValue('duration')).toBe('2h');
+ expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
+ });
+
+ describe('on start date change', () => {
+ it('changes end date on start date change if it exceeds it', fakeAsync(() => {
+ changeStartDate('2022-02-28 04:05');
+ expect(form.getValue('duration')).toEqual('2h');
+ expect(form.getValue('endsAt')).toEqual('2022-02-28 06:05');
+
+ changeStartDate('2022-12-31 22:00');
+ expect(form.getValue('duration')).toEqual('2h');
+ expect(form.getValue('endsAt')).toEqual('2023-01-01 00:00');
+ }));
+
+ it('changes duration if start date does not exceed end date ', fakeAsync(() => {
+ changeStartDate('2022-02-22 00:45');
+ expect(form.getValue('duration')).toEqual('1h 15m');
+ expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
+ }));
+
+ it('should raise invalid start date error', fakeAsync(() => {
+ changeStartDate('No valid date');
+ formHelper.expectError('startsAt', 'format');
+ expect(form.getValue('startsAt').toString()).toBe('No valid date');
+ expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
+ }));
+ });
+
+ describe('on duration change', () => {
+ it('changes end date if duration is changed', () => {
+ formHelper.setValue('duration', '15m');
+ expect(form.getValue('endsAt')).toEqual('2022-02-22 00:15');
+ formHelper.setValue('duration', '5d 23h');
+ expect(form.getValue('endsAt')).toEqual('2022-02-27 23:00');
+ });
+ });
+
+ describe('on end date change', () => {
+ it('changes duration on end date change if it exceeds start date', fakeAsync(() => {
+ changeEndDate('2022-02-28 04:05');
+ expect(form.getValue('duration')).toEqual('6d 4h 5m');
+ expect(form.getValue('startsAt')).toEqual(baseTime);
+ }));
+
+ it('changes start date if end date happens before it', fakeAsync(() => {
+ changeEndDate('2022-02-21 02:00');
+ expect(form.getValue('duration')).toEqual('2h');
+ expect(form.getValue('startsAt')).toEqual('2022-02-21 00:00');
+ }));
+
+ it('should raise invalid end date error', fakeAsync(() => {
+ changeEndDate('No valid date');
+ formHelper.expectError('endsAt', 'format');
+ expect(form.getValue('endsAt').toString()).toBe('No valid date');
+ expect(form.getValue('startsAt')).toEqual(baseTime);
+ }));
+ });
+ });
+
+ it('should have a creator field', () => {
+ formHelper.expectValid('createdBy');
+ formHelper.expectErrorChange('createdBy', '', 'required');
+ formHelper.expectValidChange('createdBy', 'Mighty FSM');
+ });
+
+ it('should have a comment field', () => {
+ formHelper.expectError('comment', 'required');
+ formHelper.expectValidChange('comment', 'A pretty long comment');
+ });
+
+ it('should be a valid form if all inputs are filled and at least one matcher was added', () => {
+ expect(form.valid).toBeFalsy();
+ formHelper.expectValidChange('createdBy', 'Mighty FSM');
+ formHelper.expectValidChange('comment', 'A pretty long comment');
+ addMatcher('job', 'someJob', false);
+ expect(form.valid).toBeTruthy();
+ });
+
+ describe('matchers', () => {
+ const expectMatch = (helpText: string) => {
+ expect(fixtureH.getText('#match-state')).toBe(helpText);
+ };
+
+ it('should show the add matcher button', () => {
+ fixtureH.expectElementVisible('#add-matcher', true);
+ fixtureH.expectIdElementsVisible(
+ [
+ 'matcher-name-0',
+ 'matcher-value-0',
+ 'matcher-isRegex-0',
+ 'matcher-edit-0',
+ 'matcher-delete-0'
+ ],
+ false
+ );
+ expectMatch(null);
+ });
+
+ it('should show added matcher', () => {
+ addMatcher('job', 'someJob', true);
+ fixtureH.expectIdElementsVisible(
+ [
+ 'matcher-name-0',
+ 'matcher-value-0',
+ 'matcher-isRegex-0',
+ 'matcher-edit-0',
+ 'matcher-delete-0'
+ ],
+ true
+ );
+ expectMatch(null);
+ });
+
+ it('should show multiple matchers', () => {
+ addMatcher('severity', 'someSeverity', false);
+ addMatcher('alertname', 'alert0', false);
+ fixtureH.expectIdElementsVisible(
+ [
+ 'matcher-name-0',
+ 'matcher-value-0',
+ 'matcher-isRegex-0',
+ 'matcher-edit-0',
+ 'matcher-delete-0',
+ 'matcher-name-1',
+ 'matcher-value-1',
+ 'matcher-isRegex-1',
+ 'matcher-edit-1',
+ 'matcher-delete-1'
+ ],
+ true
+ );
+ expectMatch('Matches 1 rule with 1 active alert.');
+ });
+
+ it('should show the right matcher values', () => {
+ addMatcher('alertname', 'alert.*', true);
+ addMatcher('job', 'someJob', false);
+ fixture.detectChanges();
+ fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
+ fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert.*');
+ fixtureH.expectFormFieldToBe('#matcher-isRegex-0', 'true');
+ fixtureH.expectFormFieldToBe('#matcher-isRegex-1', 'false');
+ expectMatch(null);
+ });
+
+ it('should be able to edit a matcher', () => {
+ addMatcher('alertname', 'alert.*', true);
+ expectMatch(null);
+
+ const modalService = TestBed.inject(ModalService);
+ spyOn(modalService, 'show').and.callFake(() => {
+ return {
+ componentInstance: {
+ preFillControls: (matcher: any) => {
+ expect(matcher).toBe(component.matchers[0]);
+ },
+ submitAction: of({ name: 'alertname', value: 'alert0', isRegex: false })
+ }
+ };
+ });
+ fixtureH.clickElement('#matcher-edit-0');
+
+ fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
+ fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert0');
+ fixtureH.expectFormFieldToBe('#matcher-isRegex-0', 'false');
+ expectMatch('Matches 1 rule with 1 active alert.');
+ });
+
+ it('should be able to remove a matcher', () => {
+ addMatcher('alertname', 'alert0', false);
+ expectMatch('Matches 1 rule with 1 active alert.');
+ fixtureH.clickElement('#matcher-delete-0');
+ expect(component.matchers).toEqual([]);
+ fixtureH.expectIdElementsVisible(
+ ['matcher-name-0', 'matcher-value-0', 'matcher-isRegex-0'],
+ false
+ );
+ expectMatch(null);
+ });
+
+ it('should be able to remove a matcher and update the matcher text', () => {
+ addMatcher('alertname', 'alert0', false);
+ addMatcher('alertname', 'alert1', false);
+ expectMatch('Your matcher seems to match no currently defined rule or active alert.');
+ fixtureH.clickElement('#matcher-delete-1');
+ expectMatch('Matches 1 rule with 1 active alert.');
+ });
+
+ it('should show form as invalid if no matcher is set', () => {
+ expect(form.errors).toEqual({ matcherRequired: true });
+ });
+
+ it('should show form as valid if matcher was added', () => {
+ addMatcher('some name', 'some value', true);
+ expect(form.errors).toEqual(null);
+ });
+ });
+
+ describe('submit tests', () => {
+ const endsAt = '2022-02-22 02:00';
+ let silence: AlertmanagerSilence;
+ const silenceId = '50M3-10N6-1D';
+
+ const expectSuccessNotification = (titleStartsWith: string) =>
+ expect(notificationService.show).toHaveBeenCalledWith(
+ NotificationType.success,
+ `${titleStartsWith} silence ${silenceId}`,
+ undefined,
+ undefined,
+ 'Prometheus'
+ );
+
+ const fillAndSubmit = () => {
+ ['createdBy', 'comment'].forEach((attr) => {
+ formHelper.setValue(attr, silence[attr]);
+ });
+ silence.matchers.forEach((matcher) =>
+ addMatcher(matcher.name, matcher.value, matcher.isRegex)
+ );
+ component.submit();
+ };
+
+ beforeEach(() => {
+ spyOn(prometheusService, 'setSilence').and.callFake(() => of({ body: { silenceId } }));
+ spyOn(router, 'navigate').and.stub();
+ silence = {
+ createdBy: 'some creator',
+ comment: 'some comment',
+ startsAt: moment(baseTime).toISOString(),
+ endsAt: moment(endsAt).toISOString(),
+ matchers: [
+ {
+ name: 'some attribute name',
+ value: 'some value',
+ isRegex: false
+ },
+ {
+ name: 'job',
+ value: 'node-exporter',
+ isRegex: false
+ },
+ {
+ name: 'instance',
+ value: 'localhost:9100',
+ isRegex: false
+ },
+ {
+ name: 'alertname',
+ value: 'load_0',
+ isRegex: false
+ }
+ ]
+ };
+ });
+
+ // it('should not create a silence if the form is invalid', () => {
+ // component.submit();
+ // expect(notificationService.show).not.toHaveBeenCalled();
+ // expect(form.valid).toBeFalsy();
+ // expect(prometheusService.setSilence).not.toHaveBeenCalledWith(silence);
+ // expect(router.navigate).not.toHaveBeenCalled();
+ // });
+
+ // it('should route back to previous tab on success', () => {
+ // fillAndSubmit();
+ // expect(form.valid).toBeTruthy();
+ // expect(router.navigate).toHaveBeenCalledWith(['/monitoring'], { fragment: 'silences' });
+ // });
+
+ it('should create a silence', () => {
+ fillAndSubmit();
+ expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
+ expectSuccessNotification('Created');
+ });
+
+ it('should recreate a silence', () => {
+ component.recreate = true;
+ component.id = 'recreateId';
+ fillAndSubmit();
+ expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
+ expectSuccessNotification('Recreated');
+ });
+
+ it('should edit a silence', () => {
+ component.edit = true;
+ component.id = 'editId';
+ silence.id = component.id;
+ fillAndSubmit();
+ expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
+ expectSuccessNotification('Edited');
+ });
+ });
+});