]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts
import 14.2.4 nautilus point release
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cluster / prometheus / silence-form / silence-form.component.spec.ts
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';
7
8 import * as _ from 'lodash';
9 import { BsDatepickerDirective, BsDatepickerModule } from 'ngx-bootstrap/datepicker';
10 import { BsModalService } from 'ngx-bootstrap/modal';
11 import { TooltipModule } from 'ngx-bootstrap/tooltip';
12 import { ToastrModule } from 'ngx-toastr';
13 import { of, throwError } from 'rxjs';
14
15 import {
16 configureTestBed,
17 FixtureHelper,
18 FormHelper,
19 i18nProviders,
20 PrometheusHelper
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';
32
33 describe('SilenceFormComponent', () => {
34 // SilenceFormComponent specific
35 let component: SilenceFormComponent;
36 let fixture: ComponentFixture<SilenceFormComponent>;
37 let form: CdFormGroup;
38 // Spied on
39 let prometheusService: PrometheusService;
40 let authStorageService: AuthStorageService;
41 let notificationService: NotificationService;
42 let router: Router;
43 // Spies
44 let rulesSpy;
45 let ifPrometheusSpy;
46 // Helper
47 let prometheus: PrometheusHelper;
48 let formH: FormHelper;
49 let fixtureH: FixtureHelper;
50 let params;
51 // Date mocking related
52 let originalDate;
53 const baseTime = new Date('2022-02-22T00:00:00');
54 const beginningDate = new Date('2022-02-22T00:00:12.35');
55
56 const routes: Routes = [{ path: '404', component: NotFoundComponent }];
57 configureTestBed({
58 declarations: [NotFoundComponent, SilenceFormComponent],
59 imports: [
60 HttpClientTestingModule,
61 RouterTestingModule.withRoutes(routes),
62 BsDatepickerModule.forRoot(),
63 SharedModule,
64 ToastrModule.forRoot(),
65 TooltipModule.forRoot(),
66 ReactiveFormsModule
67 ],
68 providers: [
69 i18nProviders,
70 {
71 provide: ActivatedRoute,
72 useValue: { params: { subscribe: (fn) => fn(params) } }
73 }
74 ]
75 });
76
77 const createMatcher = (name, value, isRegex) => ({ name, value, isRegex });
78
79 const addMatcher = (name, value, isRegex) =>
80 component['setMatcher'](createMatcher(name, value, isRegex));
81
82 const callInit = () =>
83 fixture.ngZone.run(() => {
84 component['init']();
85 });
86
87 const changeAction = (action: string) => {
88 const modes = {
89 add: '/silence/add',
90 alertAdd: '/silence/add/someAlert',
91 recreate: '/silence/recreate/someExpiredId',
92 edit: '/silence/edit/someNotExpiredId'
93 };
94 Object.defineProperty(router, 'url', { value: modes[action] });
95 callInit();
96 };
97
98 beforeEach(() => {
99 params = {};
100
101 originalDate = Date;
102 spyOn(global, 'Date').and.callFake((arg) => (arg ? new originalDate(arg) : beginningDate));
103
104 prometheus = new PrometheusHelper();
105 prometheusService = TestBed.get(PrometheusService);
106 spyOn(prometheusService, 'getAlerts').and.callFake(() =>
107 of([prometheus.createAlert('alert0')])
108 );
109 ifPrometheusSpy = spyOn(prometheusService, 'ifPrometheusConfigured').and.callFake((fn) => fn());
110 rulesSpy = spyOn(prometheusService, 'getRules').and.callFake(() =>
111 of([
112 prometheus.createRule('alert0', 'someSeverity', [prometheus.createAlert('alert0')]),
113 prometheus.createRule('alert1', 'someSeverity', []),
114 prometheus.createRule('alert2', 'someOtherSeverity', [prometheus.createAlert('alert2')])
115 ])
116 );
117
118 router = TestBed.get(Router);
119
120 notificationService = TestBed.get(NotificationService);
121 spyOn(notificationService, 'show').and.stub();
122
123 authStorageService = TestBed.get(AuthStorageService);
124 spyOn(authStorageService, 'getUsername').and.returnValue('someUser');
125
126 fixture = TestBed.createComponent(SilenceFormComponent);
127 fixtureH = new FixtureHelper(fixture);
128 component = fixture.componentInstance;
129 form = component.form;
130 formH = new FormHelper(form);
131 fixture.detectChanges();
132 });
133
134 it('should create', () => {
135 expect(component).toBeTruthy();
136 expect(_.isArray(component.rules)).toBeTruthy();
137 });
138
139 it('should have set the logged in user name as creator', () => {
140 expect(component.form.getValue('createdBy')).toBe('someUser');
141 });
142
143 it('should call disablePrometheusConfig on error calling getRules', () => {
144 spyOn(prometheusService, 'disablePrometheusConfig');
145 rulesSpy.and.callFake(() => throwError({}));
146 callInit();
147 expect(component.rules).toEqual([]);
148 expect(prometheusService.disablePrometheusConfig).toHaveBeenCalled();
149 });
150
151 it('should remind user if prometheus is not set when it is not configured', () => {
152 ifPrometheusSpy.and.callFake((_x, fn) => fn());
153 callInit();
154 expect(component.rules).toEqual([]);
155 expect(notificationService.show).toHaveBeenCalledWith(
156 NotificationType.info,
157 'Please add your Prometheus host to the dashboard configuration and refresh the page',
158 undefined,
159 undefined,
160 'Prometheus'
161 );
162 });
163
164 describe('redirect not allowed users', () => {
165 let prometheusPermissions: Permission;
166 let navigateSpy;
167
168 const expectRedirect = (action: string, redirected: boolean) => {
169 changeAction(action);
170 expect(router.navigate).toHaveBeenCalledTimes(redirected ? 1 : 0);
171 if (redirected) {
172 expect(router.navigate).toHaveBeenCalledWith(['/404']);
173 }
174 navigateSpy.calls.reset();
175 };
176
177 beforeEach(() => {
178 navigateSpy = spyOn(router, 'navigate').and.stub();
179 spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
180 prometheus: prometheusPermissions
181 }));
182 });
183
184 it('redirects to 404 if not allowed', () => {
185 prometheusPermissions = new Permission(['delete', 'read']);
186 expectRedirect('add', true);
187 expectRedirect('alertAdd', true);
188 });
189
190 it('redirects if user does not have minimum permissions to create silences', () => {
191 prometheusPermissions = new Permission(['update', 'delete', 'read']);
192 expectRedirect('add', true);
193 prometheusPermissions = new Permission(['update', 'delete', 'create']);
194 expectRedirect('recreate', true);
195 });
196
197 it('redirects if user does not have minimum permissions to update silences', () => {
198 prometheusPermissions = new Permission(['create', 'delete', 'read']);
199 expectRedirect('edit', true);
200 prometheusPermissions = new Permission(['create', 'delete', 'update']);
201 expectRedirect('edit', true);
202 });
203
204 it('does not redirect if user has minimum permissions to create silences', () => {
205 prometheusPermissions = new Permission(['create', 'read']);
206 expectRedirect('add', false);
207 expectRedirect('alertAdd', false);
208 expectRedirect('recreate', false);
209 });
210
211 it('does not redirect if user has minimum permissions to update silences', () => {
212 prometheusPermissions = new Permission(['update', 'read']);
213 expectRedirect('edit', false);
214 });
215 });
216
217 describe('choose the right action', () => {
218 const expectMode = (routerMode: string, edit: boolean, recreate: boolean, action: string) => {
219 changeAction(routerMode);
220 expect(component.recreate).toBe(recreate);
221 expect(component.edit).toBe(edit);
222 expect(component.action).toBe(action);
223 };
224
225 beforeEach(() => {
226 spyOn(prometheusService, 'getSilences').and.callFake((p) =>
227 of([prometheus.createSilence(p.id)])
228 );
229 });
230
231 it('should have no special action activate by default', () => {
232 expectMode('add', false, false, 'Create');
233 expect(prometheusService.getSilences).not.toHaveBeenCalled();
234 expect(component.form.value).toEqual({
235 comment: null,
236 createdBy: 'someUser',
237 duration: '2h',
238 startsAt: baseTime,
239 endsAt: new Date('2022-02-22T02:00:00')
240 });
241 });
242
243 it('should be in edit action if route includes edit', () => {
244 params = { id: 'someNotExpiredId' };
245 expectMode('edit', true, false, 'Edit');
246 expect(prometheusService.getSilences).toHaveBeenCalledWith(params);
247 expect(component.form.value).toEqual({
248 comment: `A comment for ${params.id}`,
249 createdBy: `Creator of ${params.id}`,
250 duration: '1d',
251 startsAt: new Date('2022-02-22T22:22:00'),
252 endsAt: new Date('2022-02-23T22:22:00')
253 });
254 expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
255 });
256
257 it('should be in recreation action if route includes recreate', () => {
258 params = { id: 'someExpiredId' };
259 expectMode('recreate', false, true, 'Recreate');
260 expect(prometheusService.getSilences).toHaveBeenCalledWith(params);
261 expect(component.form.value).toEqual({
262 comment: `A comment for ${params.id}`,
263 createdBy: `Creator of ${params.id}`,
264 duration: '2h',
265 startsAt: baseTime,
266 endsAt: new Date('2022-02-22T02:00:00')
267 });
268 expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
269 });
270
271 it('adds matchers based on the label object of the alert with the given id', () => {
272 params = { id: 'someAlert' };
273 expectMode('alertAdd', false, false, 'Create');
274 expect(prometheusService.getSilences).not.toHaveBeenCalled();
275 expect(prometheusService.getAlerts).toHaveBeenCalled();
276 expect(component.matchers).toEqual([
277 createMatcher('alertname', 'alert0', false),
278 createMatcher('instance', 'someInstance', false),
279 createMatcher('job', 'someJob', false),
280 createMatcher('severity', 'someSeverity', false)
281 ]);
282 expect(component.matcherMatch).toEqual({
283 cssClass: 'has-success',
284 status: 'Matches 1 rule with 1 active alert.'
285 });
286 });
287 });
288
289 describe('time', () => {
290 // Can't be used to set accurate UTC dates in unit tests as Date uses timezones,
291 // this means the UTC time changes depending on the timezone you are in.
292 const changeDatePicker = (el, text) => {
293 el.triggerEventHandler('change', { target: { value: text } });
294 };
295 const getDatePicker = (i) =>
296 fixture.debugElement.queryAll(By.directive(BsDatepickerDirective))[i];
297 const changeEndDate = (text) => changeDatePicker(getDatePicker(1), text);
298 const changeStartDate = (text) => changeDatePicker(getDatePicker(0), text);
299
300 it('have all dates set at beginning', () => {
301 expect(form.getValue('startsAt')).toEqual(baseTime);
302 expect(form.getValue('duration')).toBe('2h');
303 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-22T02:00:00'));
304 });
305
306 describe('on start date change', () => {
307 it('changes end date on start date change if it exceeds it', fakeAsync(() => {
308 changeStartDate('2022-02-28T 04:05');
309 expect(form.getValue('duration')).toEqual('2h');
310 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-28T06:05:00'));
311
312 changeStartDate('2022-12-31T 22:00');
313 expect(form.getValue('duration')).toEqual('2h');
314 expect(form.getValue('endsAt')).toEqual(new Date('2023-01-01T00:00:00'));
315 }));
316
317 it('changes duration if start date does not exceed end date ', fakeAsync(() => {
318 changeStartDate('2022-02-22T 00:45');
319 expect(form.getValue('duration')).toEqual('1h 15m');
320 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-22T02:00:00'));
321 }));
322
323 it('should raise invalid start date error', fakeAsync(() => {
324 changeStartDate('No valid date');
325 formH.expectError('startsAt', 'bsDate');
326 expect(form.getValue('startsAt').toString()).toBe('Invalid Date');
327 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-22T02:00:00'));
328 }));
329 });
330
331 describe('on duration change', () => {
332 it('changes end date if duration is changed', () => {
333 formH.setValue('duration', '15m');
334 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-22T00:15'));
335 formH.setValue('duration', '5d 23h');
336 expect(form.getValue('endsAt')).toEqual(new Date('2022-02-27T23:00'));
337 });
338 });
339
340 describe('on end date change', () => {
341 it('changes duration on end date change if it exceeds start date', fakeAsync(() => {
342 changeEndDate('2022-02-28T 04:05');
343 expect(form.getValue('duration')).toEqual('6d 4h 5m');
344 expect(form.getValue('startsAt')).toEqual(baseTime);
345 }));
346
347 it('changes start date if end date happens before it', fakeAsync(() => {
348 changeEndDate('2022-02-21T 02:00');
349 expect(form.getValue('duration')).toEqual('2h');
350 expect(form.getValue('startsAt')).toEqual(new Date('2022-02-21T00:00:00'));
351 }));
352
353 it('should raise invalid end date error', fakeAsync(() => {
354 changeEndDate('No valid date');
355 formH.expectError('endsAt', 'bsDate');
356 expect(form.getValue('endsAt').toString()).toBe('Invalid Date');
357 expect(form.getValue('startsAt')).toEqual(baseTime);
358 }));
359 });
360 });
361
362 it('should have a creator field', () => {
363 formH.expectValid('createdBy');
364 formH.expectErrorChange('createdBy', '', 'required');
365 formH.expectValidChange('createdBy', 'Mighty FSM');
366 });
367
368 it('should have a comment field', () => {
369 formH.expectError('comment', 'required');
370 formH.expectValidChange('comment', 'A pretty long comment');
371 });
372
373 it('should be a valid form if all inputs are filled and at least one matcher was added', () => {
374 expect(form.valid).toBeFalsy();
375 formH.expectValidChange('createdBy', 'Mighty FSM');
376 formH.expectValidChange('comment', 'A pretty long comment');
377 addMatcher('job', 'someJob', false);
378 expect(form.valid).toBeTruthy();
379 });
380
381 describe('matchers', () => {
382 const expectMatch = (helpText) => {
383 expect(fixtureH.getText('#match-state')).toBe(helpText);
384 };
385
386 it('should show the add matcher button', () => {
387 fixtureH.expectElementVisible('#add-matcher', true);
388 fixtureH.expectIdElementsVisible(
389 [
390 'matcher-name-0',
391 'matcher-value-0',
392 'matcher-isRegex-0',
393 'matcher-edit-0',
394 'matcher-delete-0'
395 ],
396 false
397 );
398 expectMatch(null);
399 });
400
401 it('should show added matcher', () => {
402 addMatcher('job', 'someJob', true);
403 fixtureH.expectIdElementsVisible(
404 [
405 'matcher-name-0',
406 'matcher-value-0',
407 'matcher-isRegex-0',
408 'matcher-edit-0',
409 'matcher-delete-0'
410 ],
411 true
412 );
413 expectMatch(null);
414 });
415
416 it('should show multiple matchers', () => {
417 addMatcher('severity', 'someSeverity', false);
418 addMatcher('alertname', 'alert0', false);
419 fixtureH.expectIdElementsVisible(
420 [
421 'matcher-name-0',
422 'matcher-value-0',
423 'matcher-isRegex-0',
424 'matcher-edit-0',
425 'matcher-delete-0',
426 'matcher-name-1',
427 'matcher-value-1',
428 'matcher-isRegex-1',
429 'matcher-edit-1',
430 'matcher-delete-1'
431 ],
432 true
433 );
434 expectMatch('Matches 1 rule with 1 active alert.');
435 });
436
437 it('should show the right matcher values', () => {
438 addMatcher('alertname', 'alert.*', true);
439 addMatcher('job', 'someJob', false);
440 fixture.detectChanges();
441 fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
442 fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert.*');
443 fixtureH.expectFormFieldToBe('#matcher-isRegex-0', 'true');
444 fixtureH.expectFormFieldToBe('#matcher-isRegex-1', 'false');
445 expectMatch(null);
446 });
447
448 it('should be able to edit a matcher', () => {
449 addMatcher('alertname', 'alert.*', true);
450 expectMatch(null);
451
452 const modalService = TestBed.get(BsModalService);
453 spyOn(modalService, 'show').and.callFake(() => {
454 return {
455 content: {
456 preFillControls: (matcher) => {
457 expect(matcher).toBe(component.matchers[0]);
458 },
459 submitAction: of({ name: 'alertname', value: 'alert0', isRegex: false })
460 }
461 };
462 });
463 fixtureH.clickElement('#matcher-edit-0');
464
465 fixtureH.expectFormFieldToBe('#matcher-name-0', 'alertname');
466 fixtureH.expectFormFieldToBe('#matcher-value-0', 'alert0');
467 fixtureH.expectFormFieldToBe('#matcher-isRegex-0', 'false');
468 expectMatch('Matches 1 rule with 1 active alert.');
469 });
470
471 it('should be able to remove a matcher', () => {
472 addMatcher('alertname', 'alert0', false);
473 expectMatch('Matches 1 rule with 1 active alert.');
474 fixtureH.clickElement('#matcher-delete-0');
475 expect(component.matchers).toEqual([]);
476 fixtureH.expectIdElementsVisible(
477 ['matcher-name-0', 'matcher-value-0', 'matcher-isRegex-0'],
478 false
479 );
480 expectMatch(null);
481 });
482
483 it('should be able to remove a matcher and update the matcher text', () => {
484 addMatcher('alertname', 'alert0', false);
485 addMatcher('alertname', 'alert1', false);
486 expectMatch('Your matcher seems to match no currently defined rule or active alert.');
487 fixtureH.clickElement('#matcher-delete-1');
488 expectMatch('Matches 1 rule with 1 active alert.');
489 });
490
491 it('should show form as invalid if no matcher is set', () => {
492 expect(form.errors).toEqual({ matcherRequired: true });
493 });
494
495 it('should show form as valid if matcher was added', () => {
496 addMatcher('some name', 'some value', true);
497 expect(form.errors).toEqual(null);
498 });
499 });
500
501 describe('submit tests', () => {
502 const endsAt = new Date('2022-02-22T02:00:00');
503 let silence: AlertmanagerSilence;
504 const silenceId = '50M3-10N6-1D';
505
506 const expectSuccessNotification = (titleStartsWith) =>
507 expect(notificationService.show).toHaveBeenCalledWith(
508 NotificationType.success,
509 `${titleStartsWith} silence ${silenceId}`,
510 undefined,
511 undefined,
512 'Prometheus'
513 );
514
515 const fillAndSubmit = () => {
516 ['createdBy', 'comment'].forEach((attr) => {
517 formH.setValue(attr, silence[attr]);
518 });
519 silence.matchers.forEach((matcher) =>
520 addMatcher(matcher.name, matcher.value, matcher.isRegex)
521 );
522 component.submit();
523 };
524
525 beforeEach(() => {
526 spyOn(prometheusService, 'setSilence').and.callFake(() => of({ body: { silenceId } }));
527 spyOn(router, 'navigate').and.stub();
528 silence = {
529 createdBy: 'some creator',
530 comment: 'some comment',
531 startsAt: baseTime.toISOString(),
532 endsAt: endsAt.toISOString(),
533 matchers: [
534 {
535 name: 'some attribute name',
536 value: 'some value',
537 isRegex: false
538 },
539 {
540 name: 'job',
541 value: 'node-exporter',
542 isRegex: false
543 },
544 {
545 name: 'instance',
546 value: 'localhost:9100',
547 isRegex: false
548 },
549 {
550 name: 'alertname',
551 value: 'load_0',
552 isRegex: false
553 }
554 ]
555 };
556 });
557
558 it('should not create a silence if the form is invalid', () => {
559 component.submit();
560 expect(notificationService.show).not.toHaveBeenCalled();
561 expect(form.valid).toBeFalsy();
562 expect(prometheusService.setSilence).not.toHaveBeenCalledWith(silence);
563 expect(router.navigate).not.toHaveBeenCalled();
564 });
565
566 it('should route back to "/silence" on success', () => {
567 fillAndSubmit();
568 expect(form.valid).toBeTruthy();
569 expect(router.navigate).toHaveBeenCalledWith(['/silence']);
570 });
571
572 it('should create a silence', () => {
573 fillAndSubmit();
574 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
575 expectSuccessNotification('Created');
576 });
577
578 it('should recreate a silence', () => {
579 component.recreate = true;
580 component.id = 'recreateId';
581 fillAndSubmit();
582 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
583 expectSuccessNotification('Recreated');
584 });
585
586 it('should edit a silence', () => {
587 component.edit = true;
588 component.id = 'editId';
589 silence.id = component.id;
590 fillAndSubmit();
591 expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
592 expectSuccessNotification('Edited');
593 });
594 });
595 });