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