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 { By } from '@angular/platform-browser';
5 import { ActivatedRoute, Router, Routes } from '@angular/router';
6 import { RouterTestingModule } from '@angular/router/testing';
8 import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
9 import * as _ from 'lodash';
10 import { BsDatepickerDirective, BsDatepickerModule } from 'ngx-bootstrap/datepicker';
11 import { BsModalService } from 'ngx-bootstrap/modal';
12 import { ToastrModule } from 'ngx-toastr';
13 import { of, throwError } from 'rxjs';
21 } from '../../../../../testing/unit-test-helper';
22 import { NotFoundComponent } from '../../../../core/not-found/not-found.component';
23 import { PrometheusService } from '../../../../shared/api/prometheus.service';
24 import { NotificationType } from '../../../../shared/enum/notification-type.enum';
25 import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
26 import { AlertmanagerSilence } from '../../../../shared/models/alertmanager-silence';
27 import { Permission } from '../../../../shared/models/permissions';
28 import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
29 import { NotificationService } from '../../../../shared/services/notification.service';
30 import { SharedModule } from '../../../../shared/shared.module';
31 import { SilenceFormComponent } from './silence-form.component';
33 describe('SilenceFormComponent', () => {
34 // SilenceFormComponent specific
35 let component: SilenceFormComponent;
36 let fixture: ComponentFixture<SilenceFormComponent>;
37 let form: CdFormGroup;
39 let prometheusService: PrometheusService;
40 let authStorageService: AuthStorageService;
41 let notificationService: NotificationService;
44 let rulesSpy: jasmine.Spy;
45 let ifPrometheusSpy: jasmine.Spy;
47 let prometheus: PrometheusHelper;
48 let formHelper: FormHelper;
49 let fixtureH: FixtureHelper;
50 let params: Record<string, any>;
51 // Date mocking related
52 let originalDate: any;
53 const baseTime = new Date('2022-02-22T00:00:00');
54 const beginningDate = new Date('2022-02-22T00:00:12.35');
56 const routes: Routes = [{ path: '404', component: NotFoundComponent }];
58 declarations: [NotFoundComponent, SilenceFormComponent],
60 HttpClientTestingModule,
61 RouterTestingModule.withRoutes(routes),
62 BsDatepickerModule.forRoot(),
64 ToastrModule.forRoot(),
71 provide: ActivatedRoute,
72 useValue: { params: { subscribe: (fn: Function) => fn(params) } }
77 const createMatcher = (name: string, value: any, isRegex: boolean) => ({ name, value, isRegex });
79 const addMatcher = (name: string, value: any, isRegex: boolean) =>
80 component['setMatcher'](createMatcher(name, value, isRegex));
82 const callInit = () =>
83 fixture.ngZone.run(() => {
87 const changeAction = (action: string) => {
89 add: '/monitoring/silence/add',
90 alertAdd: '/monitoring/silence/add/someAlert',
91 recreate: '/monitoring/silence/recreate/someExpiredId',
92 edit: '/monitoring/silence/edit/someNotExpiredId'
94 Object.defineProperty(router, 'url', { value: modes[action] });
102 spyOn(global, 'Date').and.callFake((arg) => (arg ? new originalDate(arg) : beginningDate));
104 prometheus = new PrometheusHelper();
105 prometheusService = TestBed.inject(PrometheusService);
106 spyOn(prometheusService, 'getAlerts').and.callFake(() =>
107 of([prometheus.createAlert('alert0')])
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 fixture = TestBed.createComponent(SilenceFormComponent);
138 fixtureH = new FixtureHelper(fixture);
139 component = fixture.componentInstance;
140 form = component.form;
141 formHelper = new FormHelper(form);
142 fixture.detectChanges();
145 it('should create', () => {
146 expect(component).toBeTruthy();
147 expect(_.isArray(component.rules)).toBeTruthy();
150 it('should have set the logged in user name as creator', () => {
151 expect(component.form.getValue('createdBy')).toBe('someUser');
154 it('should call disablePrometheusConfig on error calling getRules', () => {
155 spyOn(prometheusService, 'disablePrometheusConfig');
156 rulesSpy.and.callFake(() => throwError({}));
158 expect(component.rules).toEqual([]);
159 expect(prometheusService.disablePrometheusConfig).toHaveBeenCalled();
162 it('should remind user if prometheus is not set when it is not configured', () => {
163 ifPrometheusSpy.and.callFake((_x: any, fn: Function) => fn());
165 expect(component.rules).toEqual([]);
166 expect(notificationService.show).toHaveBeenCalledWith(
167 NotificationType.info,
168 'Please add your Prometheus host to the dashboard configuration and refresh the page',
175 describe('redirect not allowed users', () => {
176 let prometheusPermissions: Permission;
177 let navigateSpy: jasmine.Spy;
179 const expectRedirect = (action: string, redirected: boolean) => {
180 changeAction(action);
181 expect(router.navigate).toHaveBeenCalledTimes(redirected ? 1 : 0);
183 expect(router.navigate).toHaveBeenCalledWith(['/404']);
185 navigateSpy.calls.reset();
189 navigateSpy = spyOn(router, 'navigate').and.stub();
190 spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
191 prometheus: prometheusPermissions
195 it('redirects to 404 if not allowed', () => {
196 prometheusPermissions = new Permission(['delete', 'read']);
197 expectRedirect('add', true);
198 expectRedirect('alertAdd', true);
201 it('redirects if user does not have minimum permissions to create silences', () => {
202 prometheusPermissions = new Permission(['update', 'delete', 'read']);
203 expectRedirect('add', true);
204 prometheusPermissions = new Permission(['update', 'delete', 'create']);
205 expectRedirect('recreate', true);
208 it('redirects if user does not have minimum permissions to update silences', () => {
209 prometheusPermissions = new Permission(['create', 'delete', 'read']);
210 expectRedirect('edit', true);
211 prometheusPermissions = new Permission(['create', 'delete', 'update']);
212 expectRedirect('edit', true);
215 it('does not redirect if user has minimum permissions to create silences', () => {
216 prometheusPermissions = new Permission(['create', 'read']);
217 expectRedirect('add', false);
218 expectRedirect('alertAdd', false);
219 expectRedirect('recreate', false);
222 it('does not redirect if user has minimum permissions to update silences', () => {
223 prometheusPermissions = new Permission(['update', 'read']);
224 expectRedirect('edit', false);
228 describe('choose the right action', () => {
229 const expectMode = (routerMode: string, edit: boolean, recreate: boolean, action: string) => {
230 changeAction(routerMode);
231 expect(component.recreate).toBe(recreate);
232 expect(component.edit).toBe(edit);
233 expect(component.action).toBe(action);
237 spyOn(prometheusService, 'getSilences').and.callFake((p) =>
238 of([prometheus.createSilence(p.id)])
242 it('should have no special action activate by default', () => {
243 expectMode('add', false, false, 'Create');
244 expect(prometheusService.getSilences).not.toHaveBeenCalled();
245 expect(component.form.value).toEqual({
247 createdBy: 'someUser',
250 endsAt: new Date('2022-02-22T02:00:00')
254 it('should be in edit action if route includes edit', () => {
255 params = { id: 'someNotExpiredId' };
256 expectMode('edit', true, false, 'Edit');
257 expect(prometheusService.getSilences).toHaveBeenCalledWith(params);
258 expect(component.form.value).toEqual({
259 comment: `A comment for ${params.id}`,
260 createdBy: `Creator of ${params.id}`,
262 startsAt: new Date('2022-02-22T22:22:00'),
263 endsAt: new Date('2022-02-23T22:22:00')
265 expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
268 it('should be in recreation action if route includes recreate', () => {
269 params = { id: 'someExpiredId' };
270 expectMode('recreate', false, true, 'Recreate');
271 expect(prometheusService.getSilences).toHaveBeenCalledWith(params);
272 expect(component.form.value).toEqual({
273 comment: `A comment for ${params.id}`,
274 createdBy: `Creator of ${params.id}`,
277 endsAt: new Date('2022-02-22T02:00:00')
279 expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
282 it('adds matchers based on the label object of the alert with the given id', () => {
283 params = { id: 'someAlert' };
284 expectMode('alertAdd', false, false, 'Create');
285 expect(prometheusService.getSilences).not.toHaveBeenCalled();
286 expect(prometheusService.getAlerts).toHaveBeenCalled();
287 expect(component.matchers).toEqual([
288 createMatcher('alertname', 'alert0', false),
289 createMatcher('instance', 'someInstance', false),
290 createMatcher('job', 'someJob', false),
291 createMatcher('severity', 'someSeverity', false)
293 expect(component.matcherMatch).toEqual({
294 cssClass: 'has-success',
295 status: 'Matches 1 rule with 1 active alert.'
300 describe('time', () => {
301 // Can't be used to set accurate UTC dates in unit tests as Date uses timezones,
302 // this means the UTC time changes depending on the timezone you are in.
303 const changeDatePicker = (el: any, text: string) => {
304 el.triggerEventHandler('change', { target: { value: text } });
306 const getDatePicker = (i: number) =>
307 fixture.debugElement.queryAll(By.directive(BsDatepickerDirective))[i];
308 const changeEndDate = (text: string) => changeDatePicker(getDatePicker(1), text);
309 const changeStartDate = (text: string) => changeDatePicker(getDatePicker(0), text);
311 it('have all dates set at beginning', () => {
312 expect(form.getValue('startsAt')).toEqual(baseTime);
313 expect(form.getValue('duration')).toBe('2h');
314 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-22T02:00:00'));
317 describe('on start date change', () => {
318 it('changes end date on start date change if it exceeds it', fakeAsync(() => {
319 changeStartDate('2022-02-28T 04:05');
320 expect(form.getValue('duration')).toEqual('2h');
321 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-28T06:05:00'));
323 changeStartDate('2022-12-31T 22:00');
324 expect(form.getValue('duration')).toEqual('2h');
325 expect(form.getValue('endsAt')).toEqual(new Date('2023-01-01T00:00:00'));
328 it('changes duration if start date does not exceed end date ', fakeAsync(() => {
329 changeStartDate('2022-02-22T 00:45');
330 expect(form.getValue('duration')).toEqual('1h 15m');
331 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-22T02:00:00'));
334 it('should raise invalid start date error', fakeAsync(() => {
335 changeStartDate('No valid date');
336 formHelper.expectError('startsAt', 'bsDate');
337 expect(form.getValue('startsAt').toString()).toBe('Invalid Date');
338 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-22T02:00:00'));
342 describe('on duration change', () => {
343 it('changes end date if duration is changed', () => {
344 formHelper.setValue('duration', '15m');
345 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-22T00:15'));
346 formHelper.setValue('duration', '5d 23h');
347 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-27T23:00'));
351 describe('on end date change', () => {
352 it('changes duration on end date change if it exceeds start date', fakeAsync(() => {
353 changeEndDate('2022-02-28T 04:05');
354 expect(form.getValue('duration')).toEqual('6d 4h 5m');
355 expect(form.getValue('startsAt')).toEqual(baseTime);
358 it('changes start date if end date happens before it', fakeAsync(() => {
359 changeEndDate('2022-02-21T 02:00');
360 expect(form.getValue('duration')).toEqual('2h');
361 expect(form.getValue('startsAt')).toEqual(new Date('2022-02-21T00:00:00'));
364 it('should raise invalid end date error', fakeAsync(() => {
365 changeEndDate('No valid date');
366 formHelper.expectError('endsAt', 'bsDate');
367 expect(form.getValue('endsAt').toString()).toBe('Invalid Date');
368 expect(form.getValue('startsAt')).toEqual(baseTime);
373 it('should have a creator field', () => {
374 formHelper.expectValid('createdBy');
375 formHelper.expectErrorChange('createdBy', '', 'required');
376 formHelper.expectValidChange('createdBy', 'Mighty FSM');
379 it('should have a comment field', () => {
380 formHelper.expectError('comment', 'required');
381 formHelper.expectValidChange('comment', 'A pretty long comment');
384 it('should be a valid form if all inputs are filled and at least one matcher was added', () => {
385 expect(form.valid).toBeFalsy();
386 formHelper.expectValidChange('createdBy', 'Mighty FSM');
387 formHelper.expectValidChange('comment', 'A pretty long comment');
388 addMatcher('job', 'someJob', false);
389 expect(form.valid).toBeTruthy();
392 describe('matchers', () => {
393 const expectMatch = (helpText: string) => {
394 expect(fixtureH.getText('#match-state')).toBe(helpText);
397 it('should show the add matcher button', () => {
398 fixtureH.expectElementVisible('#add-matcher', true);
399 fixtureH.expectIdElementsVisible(
412 it('should show added matcher', () => {
413 addMatcher('job', 'someJob', true);
414 fixtureH.expectIdElementsVisible(
427 it('should show multiple matchers', () => {
428 addMatcher('severity', 'someSeverity', false);
429 addMatcher('alertname', 'alert0', false);
430 fixtureH.expectIdElementsVisible(
445 expectMatch('Matches 1 rule with 1 active alert.');
448 it('should show the right matcher values', () => {
449 addMatcher('alertname', 'alert.*', true);
450 addMatcher('job', 'someJob', false);
451 fixture.detectChanges();
452 fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
453 fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert.*');
454 fixtureH.expectFormFieldToBe('#matcher-isRegex-0', 'true');
455 fixtureH.expectFormFieldToBe('#matcher-isRegex-1', 'false');
459 it('should be able to edit a matcher', () => {
460 addMatcher('alertname', 'alert.*', true);
463 const modalService = TestBed.inject(BsModalService);
464 spyOn(modalService, 'show').and.callFake(() => {
467 preFillControls: (matcher: any) => {
468 expect(matcher).toBe(component.matchers[0]);
470 submitAction: of({ name: 'alertname', value: 'alert0', isRegex: false })
474 fixtureH.clickElement('#matcher-edit-0');
476 fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
477 fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert0');
478 fixtureH.expectFormFieldToBe('#matcher-isRegex-0', 'false');
479 expectMatch('Matches 1 rule with 1 active alert.');
482 it('should be able to remove a matcher', () => {
483 addMatcher('alertname', 'alert0', false);
484 expectMatch('Matches 1 rule with 1 active alert.');
485 fixtureH.clickElement('#matcher-delete-0');
486 expect(component.matchers).toEqual([]);
487 fixtureH.expectIdElementsVisible(
488 ['matcher-name-0', 'matcher-value-0', 'matcher-isRegex-0'],
494 it('should be able to remove a matcher and update the matcher text', () => {
495 addMatcher('alertname', 'alert0', false);
496 addMatcher('alertname', 'alert1', false);
497 expectMatch('Your matcher seems to match no currently defined rule or active alert.');
498 fixtureH.clickElement('#matcher-delete-1');
499 expectMatch('Matches 1 rule with 1 active alert.');
502 it('should show form as invalid if no matcher is set', () => {
503 expect(form.errors).toEqual({ matcherRequired: true });
506 it('should show form as valid if matcher was added', () => {
507 addMatcher('some name', 'some value', true);
508 expect(form.errors).toEqual(null);
512 describe('submit tests', () => {
513 const endsAt = new Date('2022-02-22T02:00:00');
514 let silence: AlertmanagerSilence;
515 const silenceId = '50M3-10N6-1D';
517 const expectSuccessNotification = (titleStartsWith: string) =>
518 expect(notificationService.show).toHaveBeenCalledWith(
519 NotificationType.success,
520 `${titleStartsWith} silence ${silenceId}`,
526 const fillAndSubmit = () => {
527 ['createdBy', 'comment'].forEach((attr) => {
528 formHelper.setValue(attr, silence[attr]);
530 silence.matchers.forEach((matcher) =>
531 addMatcher(matcher.name, matcher.value, matcher.isRegex)
537 spyOn(prometheusService, 'setSilence').and.callFake(() => of({ body: { silenceId } }));
538 spyOn(router, 'navigate').and.stub();
540 createdBy: 'some creator',
541 comment: 'some comment',
542 startsAt: baseTime.toISOString(),
543 endsAt: endsAt.toISOString(),
546 name: 'some attribute name',
552 value: 'node-exporter',
557 value: 'localhost:9100',
569 it('should not create a silence if the form is invalid', () => {
571 expect(notificationService.show).not.toHaveBeenCalled();
572 expect(form.valid).toBeFalsy();
573 expect(prometheusService.setSilence).not.toHaveBeenCalledWith(silence);
574 expect(router.navigate).not.toHaveBeenCalled();
577 it('should route back to previous tab on success', () => {
579 expect(form.valid).toBeTruthy();
580 expect(router.navigate).toHaveBeenCalledWith(['/monitoring'], { fragment: 'silences' });
583 it('should create a silence', () => {
585 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
586 expectSuccessNotification('Created');
589 it('should recreate a silence', () => {
590 component.recreate = true;
591 component.id = 'recreateId';
593 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
594 expectSuccessNotification('Recreated');
597 it('should edit a silence', () => {
598 component.edit = true;
599 component.id = 'editId';
600 silence.id = component.id;
602 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
603 expectSuccessNotification('Edited');