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';
20 AlertmanagerSilenceMatcher
21 } from '~/app/shared/models/alertmanager-silence';
22 import { Permission } from '~/app/shared/models/permissions';
23 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
24 import { ModalService } from '~/app/shared/services/modal.service';
25 import { NotificationService } from '~/app/shared/services/notification.service';
26 import { SharedModule } from '~/app/shared/shared.module';
32 } from '~/testing/unit-test-helper';
33 import { SilenceFormComponent } from './silence-form.component';
35 describe('SilenceFormComponent', () => {
36 // SilenceFormComponent specific
37 let component: SilenceFormComponent;
38 let fixture: ComponentFixture<SilenceFormComponent>;
39 let form: CdFormGroup;
41 let prometheusService: PrometheusService;
42 let authStorageService: AuthStorageService;
43 let notificationService: NotificationService;
46 let rulesSpy: jasmine.Spy;
47 let ifPrometheusSpy: jasmine.Spy;
49 let prometheus: PrometheusHelper;
50 let formHelper: FormHelper;
51 let fixtureH: FixtureHelper;
52 let params: Record<string, any>;
53 // Date mocking related
54 const baseTime = '2022-02-22 00:00';
55 const beginningDate = '2022-02-22T00:00:12.35';
56 let prometheusPermissions: Permission;
58 const routes: Routes = [{ path: '404', component: ErrorComponent }];
60 declarations: [ErrorComponent, SilenceFormComponent],
62 HttpClientTestingModule,
63 RouterTestingModule.withRoutes(routes),
65 ToastrModule.forRoot(),
72 provide: ActivatedRoute,
73 useValue: { params: { subscribe: (fn: Function) => fn(params) } }
78 const createMatcher = (name: string, value: any, isRegex: boolean) => ({ name, value, isRegex });
80 const addMatcher = (name: string, value: any, isRegex: boolean) =>
81 component['setMatcher'](createMatcher(name, value, isRegex));
83 const callInit = () =>
84 fixture.ngZone.run(() => {
88 const changeAction = (action: string) => {
90 add: '/monitoring/silences/add',
91 alertAdd: '/monitoring/silences/add/alert0',
92 recreate: '/monitoring/silences/recreate/someExpiredId',
93 edit: '/monitoring/silences/edit/someNotExpiredId'
95 Object.defineProperty(router, 'url', { value: modes[action] });
101 spyOn(Date, 'now').and.returnValue(new Date(beginningDate));
103 prometheus = new PrometheusHelper();
104 prometheusService = TestBed.inject(PrometheusService);
105 spyOn(prometheusService, 'getAlerts').and.callFake(() => {
106 const name = _.split(router.url, '/').pop();
107 return of([prometheus.createAlert(name)]);
109 ifPrometheusSpy = spyOn(prometheusService, 'ifPrometheusConfigured').and.callFake((fn) => fn());
110 rulesSpy = spyOn(prometheusService, 'getRules').and.callFake(() =>
118 prometheus.createRule('alert0', 'someSeverity', [prometheus.createAlert('alert0')]),
119 prometheus.createRule('alert1', 'someSeverity', []),
120 prometheus.createRule('alert2', 'someOtherSeverity', [
121 prometheus.createAlert('alert2')
129 router = TestBed.inject(Router);
131 notificationService = TestBed.inject(NotificationService);
132 spyOn(notificationService, 'show').and.stub();
134 authStorageService = TestBed.inject(AuthStorageService);
135 spyOn(authStorageService, 'getUsername').and.returnValue('someUser');
137 spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
138 prometheus: prometheusPermissions
140 prometheusPermissions = new Permission(['update', 'delete', 'read', 'create']);
141 fixture = TestBed.createComponent(SilenceFormComponent);
142 fixtureH = new FixtureHelper(fixture);
143 component = fixture.componentInstance;
144 form = component.form;
145 formHelper = new FormHelper(form);
146 fixture.detectChanges();
149 it('should create', () => {
150 expect(component).toBeTruthy();
151 expect(_.isArray(component.rules)).toBeTruthy();
154 it('should have set the logged in user name as creator', () => {
155 expect(component.form.getValue('createdBy')).toBe('someUser');
158 it('should call disablePrometheusConfig on error calling getRules', () => {
159 spyOn(prometheusService, 'disablePrometheusConfig');
160 rulesSpy.and.callFake(() => throwError({}));
162 expect(component.rules).toEqual([]);
163 expect(prometheusService.disablePrometheusConfig).toHaveBeenCalled();
166 it('should remind user if prometheus is not set when it is not configured', () => {
167 ifPrometheusSpy.and.callFake((_x: any, fn: Function) => fn());
169 expect(component.rules).toEqual([]);
170 expect(notificationService.show).toHaveBeenCalledWith(
171 NotificationType.info,
172 'Please add your Prometheus host to the dashboard configuration and refresh the page',
179 describe('throw error for not allowed users', () => {
180 let navigateSpy: jasmine.Spy;
182 const expectError = (action: string, redirected: boolean) => {
183 Object.defineProperty(router, 'url', { value: action });
185 expect(() => callInit()).toThrowError(DashboardNotFoundError);
187 expect(() => callInit()).not.toThrowError();
189 navigateSpy.calls.reset();
193 navigateSpy = spyOn(router, 'navigate').and.stub();
196 it('should throw error if not allowed', () => {
197 prometheusPermissions = new Permission(['delete', 'read']);
198 expectError('add', true);
199 expectError('alertAdd', true);
202 it('should throw error if user does not have minimum permissions to create silences', () => {
203 prometheusPermissions = new Permission(['update', 'delete', 'read']);
204 expectError('add', true);
205 prometheusPermissions = new Permission(['update', 'delete', 'create']);
206 expectError('recreate', true);
209 it('should throw error if user does not have minimum permissions to update silences', () => {
210 prometheusPermissions = new Permission(['delete', 'read']);
211 expectError('edit', true);
212 prometheusPermissions = new Permission(['create', 'delete', 'update']);
213 expectError('edit', true);
216 it('does not throw error if user has minimum permissions to create silences', () => {
217 prometheusPermissions = new Permission(['create', 'read']);
218 expectError('add', false);
219 expectError('alertAdd', false);
220 expectError('recreate', false);
223 it('does not throw error if user has minimum permissions to update silences', () => {
224 prometheusPermissions = new Permission(['read', 'create']);
225 expectError('edit', false);
229 describe('choose the right action', () => {
230 const expectMode = (routerMode: string, edit: boolean, recreate: boolean, action: string) => {
231 changeAction(routerMode);
232 expect(component.recreate).toBe(recreate);
233 expect(component.edit).toBe(edit);
234 expect(component.action).toBe(action);
238 spyOn(prometheusService, 'getSilences').and.callFake(() => {
239 const id = _.split(router.url, '/').pop();
240 return of([prometheus.createSilence(id)]);
244 it('should have no special action activate by default', () => {
245 expectMode('add', false, false, 'Create');
246 expect(prometheusService.getSilences).not.toHaveBeenCalled();
247 expect(component.form.value).toEqual({
249 createdBy: 'someUser',
252 endsAt: '2022-02-22 02:00'
256 it('should be in edit action if route includes edit', () => {
257 params = { id: 'someNotExpiredId' };
258 expectMode('edit', true, false, 'Edit');
259 expect(prometheusService.getSilences).toHaveBeenCalled();
260 expect(component.form.value).toEqual({
261 comment: `A comment for ${params.id}`,
262 createdBy: `Creator of ${params.id}`,
264 startsAt: '2022-02-22 22:22',
265 endsAt: '2022-02-23 22:22'
267 expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
270 it('should be in recreation action if route includes recreate', () => {
271 params = { id: 'someExpiredId' };
272 expectMode('recreate', false, true, 'Recreate');
273 expect(prometheusService.getSilences).toHaveBeenCalled();
274 expect(component.form.value).toEqual({
275 comment: `A comment for ${params.id}`,
276 createdBy: `Creator of ${params.id}`,
279 endsAt: '2022-02-22 02:00'
281 expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
284 it('adds matchers based on the label object of the alert with the given id', () => {
285 params = { id: 'alert0' };
286 expectMode('alertAdd', false, false, 'Create');
287 expect(prometheusService.getSilences).not.toHaveBeenCalled();
288 expect(prometheusService.getAlerts).toHaveBeenCalled();
289 expect(component.matchers).toEqual([createMatcher('alertname', 'alert0', false)]);
290 expect(component.matcherMatch).toEqual({
291 cssClass: 'has-success',
292 status: 'Matches 1 rule with 1 active alert.'
297 describe('time', () => {
298 const changeEndDate = (text: string) => component.form.patchValue({ endsAt: text });
299 const changeStartDate = (text: string) => component.form.patchValue({ startsAt: text });
301 it('have all dates set at beginning', () => {
302 expect(form.getValue('startsAt')).toEqual(baseTime);
303 expect(form.getValue('duration')).toBe('2h');
304 expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
307 describe('on start date change', () => {
308 it('changes end date on start date change if it exceeds it', fakeAsync(() => {
309 changeStartDate('2022-02-28 04:05');
310 expect(form.getValue('duration')).toEqual('2h');
311 expect(form.getValue('endsAt')).toEqual('2022-02-28 06:05');
313 changeStartDate('2022-12-31 22:00');
314 expect(form.getValue('duration')).toEqual('2h');
315 expect(form.getValue('endsAt')).toEqual('2023-01-01 00:00');
318 it('changes duration if start date does not exceed end date ', fakeAsync(() => {
319 changeStartDate('2022-02-22 00:45');
320 expect(form.getValue('duration')).toEqual('1h 15m');
321 expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
324 it('should raise invalid start date error', fakeAsync(() => {
325 changeStartDate('No valid date');
326 formHelper.expectError('startsAt', 'format');
327 expect(form.getValue('startsAt').toString()).toBe('No valid date');
328 expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
332 describe('on duration change', () => {
333 it('changes end date if duration is changed', () => {
334 formHelper.setValue('duration', '15m');
335 expect(form.getValue('endsAt')).toEqual('2022-02-22 00:15');
336 formHelper.setValue('duration', '5d 23h');
337 expect(form.getValue('endsAt')).toEqual('2022-02-27 23:00');
341 describe('on end date change', () => {
342 it('changes duration on end date change if it exceeds start date', fakeAsync(() => {
343 changeEndDate('2022-02-28 04:05');
344 expect(form.getValue('duration')).toEqual('6d 4h 5m');
345 expect(form.getValue('startsAt')).toEqual(baseTime);
348 it('changes start date if end date happens before it', fakeAsync(() => {
349 changeEndDate('2022-02-21 02:00');
350 expect(form.getValue('duration')).toEqual('2h');
351 expect(form.getValue('startsAt')).toEqual('2022-02-21 00:00');
354 it('should raise invalid end date error', fakeAsync(() => {
355 changeEndDate('No valid date');
356 formHelper.expectError('endsAt', 'format');
357 expect(form.getValue('endsAt').toString()).toBe('No valid date');
358 expect(form.getValue('startsAt')).toEqual(baseTime);
363 it('should have a creator field', () => {
364 formHelper.expectValid('createdBy');
365 formHelper.expectErrorChange('createdBy', '', 'required');
366 formHelper.expectValidChange('createdBy', 'Mighty FSM');
369 it('should have a comment field', () => {
370 formHelper.expectError('comment', 'required');
371 formHelper.expectValidChange('comment', 'A pretty long comment');
374 it('should be a valid form if all inputs are filled and at least one matcher was added', () => {
375 expect(form.valid).toBeFalsy();
376 formHelper.expectValidChange('createdBy', 'Mighty FSM');
377 formHelper.expectValidChange('comment', 'A pretty long comment');
378 addMatcher('job', 'someJob', false);
379 expect(form.valid).toBeTruthy();
382 describe('matchers', () => {
383 const expectMatch = (helpText: string) => {
384 expect(fixtureH.getText('#match-state')).toBe(helpText);
387 it('should show the add matcher button', () => {
388 fixtureH.expectElementVisible('#add-matcher', true);
389 fixtureH.expectIdElementsVisible(
402 it('should show added matcher', () => {
403 addMatcher('job', 'someJob', true);
404 fixtureH.expectIdElementsVisible(
405 ['matcher-name-0', 'matcher-value-0', 'matcher-edit-0', 'matcher-delete-0'],
411 it('should show multiple matchers', () => {
412 addMatcher('severity', 'someSeverity', false);
413 addMatcher('alertname', 'alert0', false);
414 fixtureH.expectIdElementsVisible(
427 expectMatch('Matches 1 rule with 1 active alert.');
430 it('should show the right matcher values', () => {
431 addMatcher('alertname', 'alert.*', true);
432 addMatcher('job', 'someJob', false);
433 fixture.detectChanges();
434 fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
435 fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert.*');
439 it('should be able to edit a matcher', () => {
440 addMatcher('alertname', 'alert.*', true);
443 const modalService = TestBed.inject(ModalService);
444 spyOn(modalService, 'show').and.callFake(() => {
447 preFillControls: (matcher: any) => {
448 expect(matcher).toBe(component.matchers[0]);
450 submitAction: of({ name: 'alertname', value: 'alert0', isRegex: false })
454 fixtureH.clickElement('#matcher-edit-0');
456 fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
457 fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert0');
458 expectMatch('Matches 1 rule with 1 active alert.');
461 it('should be able to remove a matcher', () => {
462 addMatcher('alertname', 'alert0', false);
463 expectMatch('Matches 1 rule with 1 active alert.');
464 fixtureH.clickElement('#matcher-delete-0');
465 expect(component.matchers).toEqual([]);
466 fixtureH.expectIdElementsVisible(
467 ['matcher-name-0', 'matcher-value-0', 'matcher-isRegex-0'],
473 it('should be able to remove a matcher and update the matcher text', () => {
474 addMatcher('alertname', 'alert0', false);
475 addMatcher('alertname', 'alert1', false);
476 expectMatch('Your matcher seems to match no currently defined rule or active alert.');
477 fixtureH.clickElement('#matcher-delete-1');
478 expectMatch('Matches 1 rule with 1 active alert.');
481 it('should show form as invalid if no matcher is set', () => {
482 expect(form.errors).toEqual({ matcherRequired: true });
485 it('should show form as valid if matcher was added', () => {
486 addMatcher('some name', 'some value', true);
487 expect(form.errors).toEqual(null);
491 describe('submit tests', () => {
492 const endsAt = '2022-02-22 02:00';
493 let silence: AlertmanagerSilence;
494 const silenceId = '50M3-10N6-1D';
496 const expectSuccessNotification = (
497 titleStartsWith: string,
498 matchers: AlertmanagerSilenceMatcher[]
501 for (const matcher of matchers) {
502 msg = msg.concat(` ${matcher.name} - ${matcher.value},`);
504 expect(notificationService.show).toHaveBeenCalledWith(
505 NotificationType.success,
506 `${titleStartsWith} silence for ${msg.slice(0, -1)}`,
513 const fillAndSubmit = () => {
514 ['createdBy', 'comment'].forEach((attr) => {
515 formHelper.setValue(attr, silence[attr]);
517 silence.matchers.forEach((matcher) =>
518 addMatcher(matcher.name, matcher.value, matcher.isRegex)
524 spyOn(prometheusService, 'setSilence').and.callFake(() => of({ body: { silenceId } }));
525 spyOn(router, 'navigate').and.stub();
527 createdBy: 'some creator',
528 comment: 'some comment',
529 startsAt: moment(baseTime).toISOString(),
530 endsAt: moment(endsAt).toISOString(),
533 name: 'some attribute name',
539 value: 'node-exporter',
544 value: 'localhost:9100',
556 // it('should not create a silence if the form is invalid', () => {
557 // component.submit();
558 // expect(notificationService.show).not.toHaveBeenCalled();
559 // expect(form.valid).toBeFalsy();
560 // expect(prometheusService.setSilence).not.toHaveBeenCalledWith(silence);
561 // expect(router.navigate).not.toHaveBeenCalled();
564 // it('should route back to previous tab on success', () => {
566 // expect(form.valid).toBeTruthy();
567 // expect(router.navigate).toHaveBeenCalledWith(['/monitoring'], { fragment: 'silences' });
570 it('should create a silence', () => {
572 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
573 expectSuccessNotification('Created', silence.matchers);
576 it('should recreate a silence', () => {
577 component.recreate = true;
578 component.id = 'recreateId';
580 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
581 expectSuccessNotification('Recreated', silence.matchers);
584 it('should edit a silence', () => {
585 component.edit = true;
586 component.id = 'editId';
587 silence.id = component.id;
589 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
590 expectSuccessNotification('Edited', silence.matchers);