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 * as _ from 'lodash';
9 import * as moment from 'moment';
10 import { ToastrModule } from 'ngx-toastr';
11 import { of, throwError } from 'rxjs';
18 } from '../../../../../testing/unit-test-helper';
19 import { NotFoundComponent } from '../../../../core/not-found/not-found.component';
20 import { PrometheusService } from '../../../../shared/api/prometheus.service';
21 import { NotificationType } from '../../../../shared/enum/notification-type.enum';
22 import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
23 import { AlertmanagerSilence } from '../../../../shared/models/alertmanager-silence';
24 import { Permission } from '../../../../shared/models/permissions';
25 import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
26 import { ModalService } from '../../../../shared/services/modal.service';
27 import { NotificationService } from '../../../../shared/services/notification.service';
28 import { SharedModule } from '../../../../shared/shared.module';
29 import { SilenceFormComponent } from './silence-form.component';
31 describe('SilenceFormComponent', () => {
32 // SilenceFormComponent specific
33 let component: SilenceFormComponent;
34 let fixture: ComponentFixture<SilenceFormComponent>;
35 let form: CdFormGroup;
37 let prometheusService: PrometheusService;
38 let authStorageService: AuthStorageService;
39 let notificationService: NotificationService;
42 let rulesSpy: jasmine.Spy;
43 let ifPrometheusSpy: jasmine.Spy;
45 let prometheus: PrometheusHelper;
46 let formHelper: FormHelper;
47 let fixtureH: FixtureHelper;
48 let params: Record<string, any>;
49 // Date mocking related
50 const baseTime = '2022-02-22 00:00';
51 const beginningDate = '2022-02-22T00:00:12.35';
53 const routes: Routes = [{ path: '404', component: NotFoundComponent }];
55 declarations: [NotFoundComponent, SilenceFormComponent],
57 HttpClientTestingModule,
58 RouterTestingModule.withRoutes(routes),
60 ToastrModule.forRoot(),
67 provide: ActivatedRoute,
68 useValue: { params: { subscribe: (fn: Function) => fn(params) } }
73 const createMatcher = (name: string, value: any, isRegex: boolean) => ({ name, value, isRegex });
75 const addMatcher = (name: string, value: any, isRegex: boolean) =>
76 component['setMatcher'](createMatcher(name, value, isRegex));
78 const callInit = () =>
79 fixture.ngZone.run(() => {
83 const changeAction = (action: string) => {
85 add: '/monitoring/silences/add',
86 alertAdd: '/monitoring/silences/add/someAlert',
87 recreate: '/monitoring/silences/recreate/someExpiredId',
88 edit: '/monitoring/silences/edit/someNotExpiredId'
90 Object.defineProperty(router, 'url', { value: modes[action] });
96 spyOn(Date, 'now').and.returnValue(new Date(beginningDate));
98 prometheus = new PrometheusHelper();
99 prometheusService = TestBed.inject(PrometheusService);
100 spyOn(prometheusService, 'getAlerts').and.callFake(() =>
101 of([prometheus.createAlert('alert0')])
103 ifPrometheusSpy = spyOn(prometheusService, 'ifPrometheusConfigured').and.callFake((fn) => fn());
104 rulesSpy = spyOn(prometheusService, 'getRules').and.callFake(() =>
112 prometheus.createRule('alert0', 'someSeverity', [prometheus.createAlert('alert0')]),
113 prometheus.createRule('alert1', 'someSeverity', []),
114 prometheus.createRule('alert2', 'someOtherSeverity', [
115 prometheus.createAlert('alert2')
123 router = TestBed.inject(Router);
125 notificationService = TestBed.inject(NotificationService);
126 spyOn(notificationService, 'show').and.stub();
128 authStorageService = TestBed.inject(AuthStorageService);
129 spyOn(authStorageService, 'getUsername').and.returnValue('someUser');
131 fixture = TestBed.createComponent(SilenceFormComponent);
132 fixtureH = new FixtureHelper(fixture);
133 component = fixture.componentInstance;
134 form = component.form;
135 formHelper = new FormHelper(form);
136 fixture.detectChanges();
139 it('should create', () => {
140 expect(component).toBeTruthy();
141 expect(_.isArray(component.rules)).toBeTruthy();
144 it('should have set the logged in user name as creator', () => {
145 expect(component.form.getValue('createdBy')).toBe('someUser');
148 it('should call disablePrometheusConfig on error calling getRules', () => {
149 spyOn(prometheusService, 'disablePrometheusConfig');
150 rulesSpy.and.callFake(() => throwError({}));
152 expect(component.rules).toEqual([]);
153 expect(prometheusService.disablePrometheusConfig).toHaveBeenCalled();
156 it('should remind user if prometheus is not set when it is not configured', () => {
157 ifPrometheusSpy.and.callFake((_x: any, fn: Function) => fn());
159 expect(component.rules).toEqual([]);
160 expect(notificationService.show).toHaveBeenCalledWith(
161 NotificationType.info,
162 'Please add your Prometheus host to the dashboard configuration and refresh the page',
169 describe('redirect not allowed users', () => {
170 let prometheusPermissions: Permission;
171 let navigateSpy: jasmine.Spy;
173 const expectRedirect = (action: string, redirected: boolean) => {
174 changeAction(action);
175 expect(router.navigate).toHaveBeenCalledTimes(redirected ? 1 : 0);
177 expect(router.navigate).toHaveBeenCalledWith(['/404']);
179 navigateSpy.calls.reset();
183 navigateSpy = spyOn(router, 'navigate').and.stub();
184 spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
185 prometheus: prometheusPermissions
189 it('redirects to 404 if not allowed', () => {
190 prometheusPermissions = new Permission(['delete', 'read']);
191 expectRedirect('add', true);
192 expectRedirect('alertAdd', true);
195 it('redirects if user does not have minimum permissions to create silences', () => {
196 prometheusPermissions = new Permission(['update', 'delete', 'read']);
197 expectRedirect('add', true);
198 prometheusPermissions = new Permission(['update', 'delete', 'create']);
199 expectRedirect('recreate', true);
202 it('redirects if user does not have minimum permissions to update silences', () => {
203 prometheusPermissions = new Permission(['create', 'delete', 'read']);
204 expectRedirect('edit', true);
205 prometheusPermissions = new Permission(['create', 'delete', 'update']);
206 expectRedirect('edit', true);
209 it('does not redirect if user has minimum permissions to create silences', () => {
210 prometheusPermissions = new Permission(['create', 'read']);
211 expectRedirect('add', false);
212 expectRedirect('alertAdd', false);
213 expectRedirect('recreate', false);
216 it('does not redirect if user has minimum permissions to update silences', () => {
217 prometheusPermissions = new Permission(['update', 'read']);
218 expectRedirect('edit', false);
222 describe('choose the right action', () => {
223 const expectMode = (routerMode: string, edit: boolean, recreate: boolean, action: string) => {
224 changeAction(routerMode);
225 expect(component.recreate).toBe(recreate);
226 expect(component.edit).toBe(edit);
227 expect(component.action).toBe(action);
231 spyOn(prometheusService, 'getSilences').and.callFake((p) =>
232 of([prometheus.createSilence(p.id)])
236 it('should have no special action activate by default', () => {
237 expectMode('add', false, false, 'Create');
238 expect(prometheusService.getSilences).not.toHaveBeenCalled();
239 expect(component.form.value).toEqual({
241 createdBy: 'someUser',
244 endsAt: '2022-02-22 02:00'
248 it('should be in edit action if route includes edit', () => {
249 params = { id: 'someNotExpiredId' };
250 expectMode('edit', true, false, 'Edit');
251 expect(prometheusService.getSilences).toHaveBeenCalledWith(params);
252 expect(component.form.value).toEqual({
253 comment: `A comment for ${params.id}`,
254 createdBy: `Creator of ${params.id}`,
256 startsAt: '2022-02-22 22:22',
257 endsAt: '2022-02-23 22:22'
259 expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
262 it('should be in recreation action if route includes recreate', () => {
263 params = { id: 'someExpiredId' };
264 expectMode('recreate', false, true, 'Recreate');
265 expect(prometheusService.getSilences).toHaveBeenCalledWith(params);
266 expect(component.form.value).toEqual({
267 comment: `A comment for ${params.id}`,
268 createdBy: `Creator of ${params.id}`,
271 endsAt: '2022-02-22 02:00'
273 expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
276 it('adds matchers based on the label object of the alert with the given id', () => {
277 params = { id: 'someAlert' };
278 expectMode('alertAdd', false, false, 'Create');
279 expect(prometheusService.getSilences).not.toHaveBeenCalled();
280 expect(prometheusService.getAlerts).toHaveBeenCalled();
281 expect(component.matchers).toEqual([
282 createMatcher('alertname', 'alert0', false),
283 createMatcher('instance', 'someInstance', false),
284 createMatcher('job', 'someJob', false),
285 createMatcher('severity', 'someSeverity', false)
287 expect(component.matcherMatch).toEqual({
288 cssClass: 'has-success',
289 status: 'Matches 1 rule with 1 active alert.'
294 describe('time', () => {
295 const changeEndDate = (text: string) => component.form.patchValue({ endsAt: text });
296 const changeStartDate = (text: string) => component.form.patchValue({ startsAt: text });
298 it('have all dates set at beginning', () => {
299 expect(form.getValue('startsAt')).toEqual(baseTime);
300 expect(form.getValue('duration')).toBe('2h');
301 expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
304 describe('on start date change', () => {
305 it('changes end date on start date change if it exceeds it', fakeAsync(() => {
306 changeStartDate('2022-02-28 04:05');
307 expect(form.getValue('duration')).toEqual('2h');
308 expect(form.getValue('endsAt')).toEqual('2022-02-28 06:05');
310 changeStartDate('2022-12-31 22:00');
311 expect(form.getValue('duration')).toEqual('2h');
312 expect(form.getValue('endsAt')).toEqual('2023-01-01 00:00');
315 it('changes duration if start date does not exceed end date ', fakeAsync(() => {
316 changeStartDate('2022-02-22 00:45');
317 expect(form.getValue('duration')).toEqual('1h 15m');
318 expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
321 it('should raise invalid start date error', fakeAsync(() => {
322 changeStartDate('No valid date');
323 formHelper.expectError('startsAt', 'format');
324 expect(form.getValue('startsAt').toString()).toBe('No valid date');
325 expect(form.getValue('endsAt')).toEqual('2022-02-22 02:00');
329 describe('on duration change', () => {
330 it('changes end date if duration is changed', () => {
331 formHelper.setValue('duration', '15m');
332 expect(form.getValue('endsAt')).toEqual('2022-02-22 00:15');
333 formHelper.setValue('duration', '5d 23h');
334 expect(form.getValue('endsAt')).toEqual('2022-02-27 23:00');
338 describe('on end date change', () => {
339 it('changes duration on end date change if it exceeds start date', fakeAsync(() => {
340 changeEndDate('2022-02-28 04:05');
341 expect(form.getValue('duration')).toEqual('6d 4h 5m');
342 expect(form.getValue('startsAt')).toEqual(baseTime);
345 it('changes start date if end date happens before it', fakeAsync(() => {
346 changeEndDate('2022-02-21 02:00');
347 expect(form.getValue('duration')).toEqual('2h');
348 expect(form.getValue('startsAt')).toEqual('2022-02-21 00:00');
351 it('should raise invalid end date error', fakeAsync(() => {
352 changeEndDate('No valid date');
353 formHelper.expectError('endsAt', 'format');
354 expect(form.getValue('endsAt').toString()).toBe('No valid date');
355 expect(form.getValue('startsAt')).toEqual(baseTime);
360 it('should have a creator field', () => {
361 formHelper.expectValid('createdBy');
362 formHelper.expectErrorChange('createdBy', '', 'required');
363 formHelper.expectValidChange('createdBy', 'Mighty FSM');
366 it('should have a comment field', () => {
367 formHelper.expectError('comment', 'required');
368 formHelper.expectValidChange('comment', 'A pretty long comment');
371 it('should be a valid form if all inputs are filled and at least one matcher was added', () => {
372 expect(form.valid).toBeFalsy();
373 formHelper.expectValidChange('createdBy', 'Mighty FSM');
374 formHelper.expectValidChange('comment', 'A pretty long comment');
375 addMatcher('job', 'someJob', false);
376 expect(form.valid).toBeTruthy();
379 describe('matchers', () => {
380 const expectMatch = (helpText: string) => {
381 expect(fixtureH.getText('#match-state')).toBe(helpText);
384 it('should show the add matcher button', () => {
385 fixtureH.expectElementVisible('#add-matcher', true);
386 fixtureH.expectIdElementsVisible(
399 it('should show added matcher', () => {
400 addMatcher('job', 'someJob', true);
401 fixtureH.expectIdElementsVisible(
414 it('should show multiple matchers', () => {
415 addMatcher('severity', 'someSeverity', false);
416 addMatcher('alertname', 'alert0', false);
417 fixtureH.expectIdElementsVisible(
432 expectMatch('Matches 1 rule with 1 active alert.');
435 it('should show the right matcher values', () => {
436 addMatcher('alertname', 'alert.*', true);
437 addMatcher('job', 'someJob', false);
438 fixture.detectChanges();
439 fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
440 fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert.*');
441 fixtureH.expectFormFieldToBe('#matcher-isRegex-0', 'true');
442 fixtureH.expectFormFieldToBe('#matcher-isRegex-1', 'false');
446 it('should be able to edit a matcher', () => {
447 addMatcher('alertname', 'alert.*', true);
450 const modalService = TestBed.inject(ModalService);
451 spyOn(modalService, 'show').and.callFake(() => {
454 preFillControls: (matcher: any) => {
455 expect(matcher).toBe(component.matchers[0]);
457 submitAction: of({ name: 'alertname', value: 'alert0', isRegex: false })
461 fixtureH.clickElement('#matcher-edit-0');
463 fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
464 fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert0');
465 fixtureH.expectFormFieldToBe('#matcher-isRegex-0', 'false');
466 expectMatch('Matches 1 rule with 1 active alert.');
469 it('should be able to remove a matcher', () => {
470 addMatcher('alertname', 'alert0', false);
471 expectMatch('Matches 1 rule with 1 active alert.');
472 fixtureH.clickElement('#matcher-delete-0');
473 expect(component.matchers).toEqual([]);
474 fixtureH.expectIdElementsVisible(
475 ['matcher-name-0', 'matcher-value-0', 'matcher-isRegex-0'],
481 it('should be able to remove a matcher and update the matcher text', () => {
482 addMatcher('alertname', 'alert0', false);
483 addMatcher('alertname', 'alert1', false);
484 expectMatch('Your matcher seems to match no currently defined rule or active alert.');
485 fixtureH.clickElement('#matcher-delete-1');
486 expectMatch('Matches 1 rule with 1 active alert.');
489 it('should show form as invalid if no matcher is set', () => {
490 expect(form.errors).toEqual({ matcherRequired: true });
493 it('should show form as valid if matcher was added', () => {
494 addMatcher('some name', 'some value', true);
495 expect(form.errors).toEqual(null);
499 describe('submit tests', () => {
500 const endsAt = '2022-02-22 02:00';
501 let silence: AlertmanagerSilence;
502 const silenceId = '50M3-10N6-1D';
504 const expectSuccessNotification = (titleStartsWith: string) =>
505 expect(notificationService.show).toHaveBeenCalledWith(
506 NotificationType.success,
507 `${titleStartsWith} silence ${silenceId}`,
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');
576 it('should recreate a silence', () => {
577 component.recreate = true;
578 component.id = 'recreateId';
580 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
581 expectSuccessNotification('Recreated');
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');