1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
3 import { ReactiveFormsModule } from '@angular/forms';
4 import { ActivatedRoute, Router, Routes } from '@angular/router';
5 import { RouterTestingModule } from '@angular/router/testing';
7 import { NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
8 import _ from 'lodash';
9 import moment from 'moment';
10 import { ToastrModule } from 'ngx-toastr';
11 import { of, throwError } from 'rxjs';
13 import { DashboardNotFoundError } from '~/app/core/error/error';
14 import { ErrorComponent } from '~/app/core/error/error.component';
15 import { PrometheusService } from '~/app/shared/api/prometheus.service';
16 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
17 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
18 import { AlertmanagerSilence } from '~/app/shared/models/alertmanager-silence';
19 import { Permission } from '~/app/shared/models/permissions';
20 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
21 import { ModalService } from '~/app/shared/services/modal.service';
22 import { NotificationService } from '~/app/shared/services/notification.service';
23 import { SharedModule } from '~/app/shared/shared.module';
29 } from '~/testing/unit-test-helper';
30 import { SilenceFormComponent } from './silence-form.component';
32 describe('SilenceFormComponent', () => {
33 // SilenceFormComponent specific
34 let component: SilenceFormComponent;
35 let fixture: ComponentFixture<SilenceFormComponent>;
36 let form: CdFormGroup;
38 let prometheusService: PrometheusService;
39 let authStorageService: AuthStorageService;
40 let notificationService: NotificationService;
43 let rulesSpy: jasmine.Spy;
44 let ifPrometheusSpy: jasmine.Spy;
46 let prometheus: PrometheusHelper;
47 let formHelper: FormHelper;
48 let fixtureH: FixtureHelper;
49 let params: Record<string, any>;
50 // Date mocking related
51 const baseTime = '2022-02-22 00:00';
52 const beginningDate = '2022-02-22T00:00:12.35';
53 let prometheusPermissions: Permission;
55 const routes: Routes = [{ path: '404', component: ErrorComponent }];
57 declarations: [ErrorComponent, SilenceFormComponent],
59 HttpClientTestingModule,
60 RouterTestingModule.withRoutes(routes),
62 ToastrModule.forRoot(),
69 provide: ActivatedRoute,
70 useValue: { params: { subscribe: (fn: Function) => fn(params) } }
75 const createMatcher = (name: string, value: any, isRegex: boolean) => ({ name, value, isRegex });
77 const addMatcher = (name: string, value: any, isRegex: boolean) =>
78 component['setMatcher'](createMatcher(name, value, isRegex));
80 const callInit = () =>
81 fixture.ngZone.run(() => {
85 const changeAction = (action: string) => {
87 add: '/monitoring/silences/add',
88 alertAdd: '/monitoring/silences/add/alert0',
89 recreate: '/monitoring/silences/recreate/someExpiredId',
90 edit: '/monitoring/silences/edit/someNotExpiredId'
92 Object.defineProperty(router, 'url', { value: modes[action] });
98 spyOn(Date, 'now').and.returnValue(new Date(beginningDate));
100 prometheus = new PrometheusHelper();
101 prometheusService = TestBed.inject(PrometheusService);
102 spyOn(prometheusService, 'getAlerts').and.callFake(() => {
103 const name = _.split(router.url, '/').pop();
104 return of([prometheus.createAlert(name)]);
106 ifPrometheusSpy = spyOn(prometheusService, 'ifPrometheusConfigured').and.callFake((fn) => fn());
107 rulesSpy = spyOn(prometheusService, 'getRules').and.callFake(() =>
115 prometheus.createRule('alert0', 'someSeverity', [prometheus.createAlert('alert0')]),
116 prometheus.createRule('alert1', 'someSeverity', []),
117 prometheus.createRule('alert2', 'someOtherSeverity', [
118 prometheus.createAlert('alert2')
126 router = TestBed.inject(Router);
128 notificationService = TestBed.inject(NotificationService);
129 spyOn(notificationService, 'show').and.stub();
131 authStorageService = TestBed.inject(AuthStorageService);
132 spyOn(authStorageService, 'getUsername').and.returnValue('someUser');
134 spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
135 prometheus: prometheusPermissions
137 prometheusPermissions = new Permission(['update', 'delete', 'read', 'create']);
138 fixture = TestBed.createComponent(SilenceFormComponent);
139 fixtureH = new FixtureHelper(fixture);
140 component = fixture.componentInstance;
141 form = component.form;
142 formHelper = new FormHelper(form);
143 fixture.detectChanges();
146 it('should create', () => {
147 expect(component).toBeTruthy();
148 expect(_.isArray(component.rules)).toBeTruthy();
151 it('should have set the logged in user name as creator', () => {
152 expect(component.form.getValue('createdBy')).toBe('someUser');
155 it('should call disablePrometheusConfig on error calling getRules', () => {
156 spyOn(prometheusService, 'disablePrometheusConfig');
157 rulesSpy.and.callFake(() => throwError({}));
159 expect(component.rules).toEqual([]);
160 expect(prometheusService.disablePrometheusConfig).toHaveBeenCalled();
163 it('should remind user if prometheus is not set when it is not configured', () => {
164 ifPrometheusSpy.and.callFake((_x: any, fn: Function) => fn());
166 expect(component.rules).toEqual([]);
167 expect(notificationService.show).toHaveBeenCalledWith(
168 NotificationType.info,
169 'Please add your Prometheus host to the dashboard configuration and refresh the page',
176 describe('throw error for not allowed users', () => {
177 let navigateSpy: jasmine.Spy;
179 const expectError = (action: string, redirected: boolean) => {
180 Object.defineProperty(router, 'url', { value: action });
182 expect(() => callInit()).toThrowError(DashboardNotFoundError);
184 expect(() => callInit()).not.toThrowError();
186 navigateSpy.calls.reset();
190 navigateSpy = spyOn(router, 'navigate').and.stub();
193 it('should throw error if not allowed', () => {
194 prometheusPermissions = new Permission(['delete', 'read']);
195 expectError('add', true);
196 expectError('alertAdd', true);
199 it('should throw error if user does not have minimum permissions to create silences', () => {
200 prometheusPermissions = new Permission(['update', 'delete', 'read']);
201 expectError('add', true);
202 prometheusPermissions = new Permission(['update', 'delete', 'create']);
203 expectError('recreate', true);
206 it('should throw error if user does not have minimum permissions to update silences', () => {
207 prometheusPermissions = new Permission(['delete', 'read']);
208 expectError('edit', true);
209 prometheusPermissions = new Permission(['create', 'delete', 'update']);
210 expectError('edit', true);
213 it('does not throw error if user has minimum permissions to create silences', () => {
214 prometheusPermissions = new Permission(['create', 'read']);
215 expectError('add', false);
216 expectError('alertAdd', false);
217 expectError('recreate', false);
220 it('does not throw error if user has minimum permissions to update silences', () => {
221 prometheusPermissions = new Permission(['read', 'create']);
222 expectError('edit', false);
226 describe('choose the right action', () => {
227 const expectMode = (routerMode: string, edit: boolean, recreate: boolean, action: string) => {
228 changeAction(routerMode);
229 expect(component.recreate).toBe(recreate);
230 expect(component.edit).toBe(edit);
231 expect(component.action).toBe(action);
235 spyOn(prometheusService, 'getSilences').and.callFake(() => {
236 const id = _.split(router.url, '/').pop();
237 return of([prometheus.createSilence(id)]);
241 it('should have no special action activate by default', () => {
242 expectMode('add', false, false, 'Create');
243 expect(prometheusService.getSilences).not.toHaveBeenCalled();
244 expect(component.form.value).toEqual({
246 createdBy: 'someUser',
249 endsAt: '2022-02-22 02:00'
253 it('should be in edit action if route includes edit', () => {
254 params = { id: 'someNotExpiredId' };
255 expectMode('edit', true, false, 'Edit');
256 expect(prometheusService.getSilences).toHaveBeenCalled();
257 expect(component.form.value).toEqual({
258 comment: `A comment for ${params.id}`,
259 createdBy: `Creator of ${params.id}`,
261 startsAt: '2022-02-22 22:22',
262 endsAt: '2022-02-23 22:22'
264 expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
267 it('should be in recreation action if route includes recreate', () => {
268 params = { id: 'someExpiredId' };
269 expectMode('recreate', false, true, 'Recreate');
270 expect(prometheusService.getSilences).toHaveBeenCalled();
271 expect(component.form.value).toEqual({
272 comment: `A comment for ${params.id}`,
273 createdBy: `Creator of ${params.id}`,
276 endsAt: '2022-02-22 02:00'
278 expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
281 it('adds matchers based on the label object of the alert with the given id', () => {
282 params = { id: 'alert0' };
283 expectMode('alertAdd', false, false, 'Create');
284 expect(prometheusService.getSilences).not.toHaveBeenCalled();
285 expect(prometheusService.getAlerts).toHaveBeenCalled();
286 expect(component.matchers).toEqual([
287 createMatcher('alertname', 'alert0', false),
288 createMatcher('instance', 'someInstance', false),
289 createMatcher('job', 'someJob', false),
290 createMatcher('severity', 'someSeverity', false)
292 expect(component.matcherMatch).toEqual({
293 cssClass: 'has-success',
294 status: 'Matches 1 rule with 1 active alert.'
299 describe('time', () => {
300 const changeEndDate = (text: string) => component.form.patchValue({ endsAt: text });
301 const changeStartDate = (text: string) => component.form.patchValue({ startsAt: text });
303 it('have all dates set at beginning', () => {
304 expect(form.getValue('startsAt')).toEqual(baseTime);
305 expect(form.getValue('duration')).toBe('2h');
306 expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
309 describe('on start date change', () => {
310 it('changes end date on start date change if it exceeds it', fakeAsync(() => {
311 changeStartDate('2022-02-28 04:05');
312 expect(form.getValue('duration')).toEqual('2h');
313 expect(form.getValue('endsAt')).toEqual('2022-02-28 06:05');
315 changeStartDate('2022-12-31 22:00');
316 expect(form.getValue('duration')).toEqual('2h');
317 expect(form.getValue('endsAt')).toEqual('2023-01-01 00:00');
320 it('changes duration if start date does not exceed end date ', fakeAsync(() => {
321 changeStartDate('2022-02-22 00:45');
322 expect(form.getValue('duration')).toEqual('1h 15m');
323 expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
326 it('should raise invalid start date error', fakeAsync(() => {
327 changeStartDate('No valid date');
328 formHelper.expectError('startsAt', 'format');
329 expect(form.getValue('startsAt').toString()).toBe('No valid date');
330 expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
334 describe('on duration change', () => {
335 it('changes end date if duration is changed', () => {
336 formHelper.setValue('duration', '15m');
337 expect(form.getValue('endsAt')).toEqual('2022-02-22 00:15');
338 formHelper.setValue('duration', '5d 23h');
339 expect(form.getValue('endsAt')).toEqual('2022-02-27 23:00');
343 describe('on end date change', () => {
344 it('changes duration on end date change if it exceeds start date', fakeAsync(() => {
345 changeEndDate('2022-02-28 04:05');
346 expect(form.getValue('duration')).toEqual('6d 4h 5m');
347 expect(form.getValue('startsAt')).toEqual(baseTime);
350 it('changes start date if end date happens before it', fakeAsync(() => {
351 changeEndDate('2022-02-21 02:00');
352 expect(form.getValue('duration')).toEqual('2h');
353 expect(form.getValue('startsAt')).toEqual('2022-02-21 00:00');
356 it('should raise invalid end date error', fakeAsync(() => {
357 changeEndDate('No valid date');
358 formHelper.expectError('endsAt', 'format');
359 expect(form.getValue('endsAt').toString()).toBe('No valid date');
360 expect(form.getValue('startsAt')).toEqual(baseTime);
365 it('should have a creator field', () => {
366 formHelper.expectValid('createdBy');
367 formHelper.expectErrorChange('createdBy', '', 'required');
368 formHelper.expectValidChange('createdBy', 'Mighty FSM');
371 it('should have a comment field', () => {
372 formHelper.expectError('comment', 'required');
373 formHelper.expectValidChange('comment', 'A pretty long comment');
376 it('should be a valid form if all inputs are filled and at least one matcher was added', () => {
377 expect(form.valid).toBeFalsy();
378 formHelper.expectValidChange('createdBy', 'Mighty FSM');
379 formHelper.expectValidChange('comment', 'A pretty long comment');
380 addMatcher('job', 'someJob', false);
381 expect(form.valid).toBeTruthy();
384 describe('matchers', () => {
385 const expectMatch = (helpText: string) => {
386 expect(fixtureH.getText('#match-state')).toBe(helpText);
389 it('should show the add matcher button', () => {
390 fixtureH.expectElementVisible('#add-matcher', true);
391 fixtureH.expectIdElementsVisible(
404 it('should show added matcher', () => {
405 addMatcher('job', 'someJob', true);
406 fixtureH.expectIdElementsVisible(
419 it('should show multiple matchers', () => {
420 addMatcher('severity', 'someSeverity', false);
421 addMatcher('alertname', 'alert0', false);
422 fixtureH.expectIdElementsVisible(
437 expectMatch('Matches 1 rule with 1 active alert.');
440 it('should show the right matcher values', () => {
441 addMatcher('alertname', 'alert.*', true);
442 addMatcher('job', 'someJob', false);
443 fixture.detectChanges();
444 fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
445 fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert.*');
446 fixtureH.expectFormFieldToBe('#matcher-isRegex-0', 'true');
447 fixtureH.expectFormFieldToBe('#matcher-isRegex-1', 'false');
451 it('should be able to edit a matcher', () => {
452 addMatcher('alertname', 'alert.*', true);
455 const modalService = TestBed.inject(ModalService);
456 spyOn(modalService, 'show').and.callFake(() => {
459 preFillControls: (matcher: any) => {
460 expect(matcher).toBe(component.matchers[0]);
462 submitAction: of({ name: 'alertname', value: 'alert0', isRegex: false })
466 fixtureH.clickElement('#matcher-edit-0');
468 fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
469 fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert0');
470 fixtureH.expectFormFieldToBe('#matcher-isRegex-0', 'false');
471 expectMatch('Matches 1 rule with 1 active alert.');
474 it('should be able to remove a matcher', () => {
475 addMatcher('alertname', 'alert0', false);
476 expectMatch('Matches 1 rule with 1 active alert.');
477 fixtureH.clickElement('#matcher-delete-0');
478 expect(component.matchers).toEqual([]);
479 fixtureH.expectIdElementsVisible(
480 ['matcher-name-0', 'matcher-value-0', 'matcher-isRegex-0'],
486 it('should be able to remove a matcher and update the matcher text', () => {
487 addMatcher('alertname', 'alert0', false);
488 addMatcher('alertname', 'alert1', false);
489 expectMatch('Your matcher seems to match no currently defined rule or active alert.');
490 fixtureH.clickElement('#matcher-delete-1');
491 expectMatch('Matches 1 rule with 1 active alert.');
494 it('should show form as invalid if no matcher is set', () => {
495 expect(form.errors).toEqual({ matcherRequired: true });
498 it('should show form as valid if matcher was added', () => {
499 addMatcher('some name', 'some value', true);
500 expect(form.errors).toEqual(null);
504 describe('submit tests', () => {
505 const endsAt = '2022-02-22 02:00';
506 let silence: AlertmanagerSilence;
507 const silenceId = '50M3-10N6-1D';
509 const expectSuccessNotification = (titleStartsWith: string) =>
510 expect(notificationService.show).toHaveBeenCalledWith(
511 NotificationType.success,
512 `${titleStartsWith} silence ${silenceId}`,
518 const fillAndSubmit = () => {
519 ['createdBy', 'comment'].forEach((attr) => {
520 formHelper.setValue(attr, silence[attr]);
522 silence.matchers.forEach((matcher) =>
523 addMatcher(matcher.name, matcher.value, matcher.isRegex)
529 spyOn(prometheusService, 'setSilence').and.callFake(() => of({ body: { silenceId } }));
530 spyOn(router, 'navigate').and.stub();
532 createdBy: 'some creator',
533 comment: 'some comment',
534 startsAt: moment(baseTime).toISOString(),
535 endsAt: moment(endsAt).toISOString(),
538 name: 'some attribute name',
544 value: 'node-exporter',
549 value: 'localhost:9100',
561 // it('should not create a silence if the form is invalid', () => {
562 // component.submit();
563 // expect(notificationService.show).not.toHaveBeenCalled();
564 // expect(form.valid).toBeFalsy();
565 // expect(prometheusService.setSilence).not.toHaveBeenCalledWith(silence);
566 // expect(router.navigate).not.toHaveBeenCalled();
569 // it('should route back to previous tab on success', () => {
571 // expect(form.valid).toBeTruthy();
572 // expect(router.navigate).toHaveBeenCalledWith(['/monitoring'], { fragment: 'silences' });
575 it('should create a silence', () => {
577 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
578 expectSuccessNotification('Created');
581 it('should recreate a silence', () => {
582 component.recreate = true;
583 component.id = 'recreateId';
585 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
586 expectSuccessNotification('Recreated');
589 it('should edit a silence', () => {
590 component.edit = true;
591 component.id = 'editId';
592 silence.id = component.id;
594 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
595 expectSuccessNotification('Edited');