]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/blob
ee3d5e956e36410d82cbc6c2e0d2d5e3ad0f2cc1
[ceph-ci.git] /
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';
6
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';
12
13 import {
14   configureTestBed,
15   FixtureHelper,
16   FormHelper,
17   PrometheusHelper
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';
30
31 describe('SilenceFormComponent', () => {
32   // SilenceFormComponent specific
33   let component: SilenceFormComponent;
34   let fixture: ComponentFixture<SilenceFormComponent>;
35   let form: CdFormGroup;
36   // Spied on
37   let prometheusService: PrometheusService;
38   let authStorageService: AuthStorageService;
39   let notificationService: NotificationService;
40   let router: Router;
41   // Spies
42   let rulesSpy: jasmine.Spy;
43   let ifPrometheusSpy: jasmine.Spy;
44   // Helper
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';
52
53   const routes: Routes = [{ path: '404', component: NotFoundComponent }];
54   configureTestBed({
55     declarations: [NotFoundComponent, SilenceFormComponent],
56     imports: [
57       HttpClientTestingModule,
58       RouterTestingModule.withRoutes(routes),
59       SharedModule,
60       ToastrModule.forRoot(),
61       NgbTooltipModule,
62       NgbPopoverModule,
63       ReactiveFormsModule
64     ],
65     providers: [
66       {
67         provide: ActivatedRoute,
68         useValue: { params: { subscribe: (fn: Function) => fn(params) } }
69       }
70     ]
71   });
72
73   const createMatcher = (name: string, value: any, isRegex: boolean) => ({ name, value, isRegex });
74
75   const addMatcher = (name: string, value: any, isRegex: boolean) =>
76     component['setMatcher'](createMatcher(name, value, isRegex));
77
78   const callInit = () =>
79     fixture.ngZone.run(() => {
80       component['init']();
81     });
82
83   const changeAction = (action: string) => {
84     const modes = {
85       add: '/monitoring/silences/add',
86       alertAdd: '/monitoring/silences/add/someAlert',
87       recreate: '/monitoring/silences/recreate/someExpiredId',
88       edit: '/monitoring/silences/edit/someNotExpiredId'
89     };
90     Object.defineProperty(router, 'url', { value: modes[action] });
91     callInit();
92   };
93
94   beforeEach(() => {
95     params = {};
96     spyOn(Date, 'now').and.returnValue(new Date(beginningDate));
97
98     prometheus = new PrometheusHelper();
99     prometheusService = TestBed.inject(PrometheusService);
100     spyOn(prometheusService, 'getAlerts').and.callFake(() =>
101       of([prometheus.createAlert('alert0')])
102     );
103     ifPrometheusSpy = spyOn(prometheusService, 'ifPrometheusConfigured').and.callFake((fn) => fn());
104     rulesSpy = spyOn(prometheusService, 'getRules').and.callFake(() =>
105       of({
106         groups: [
107           {
108             file: '',
109             interval: 0,
110             name: '',
111             rules: [
112               prometheus.createRule('alert0', 'someSeverity', [prometheus.createAlert('alert0')]),
113               prometheus.createRule('alert1', 'someSeverity', []),
114               prometheus.createRule('alert2', 'someOtherSeverity', [
115                 prometheus.createAlert('alert2')
116               ])
117             ]
118           }
119         ]
120       })
121     );
122
123     router = TestBed.inject(Router);
124
125     notificationService = TestBed.inject(NotificationService);
126     spyOn(notificationService, 'show').and.stub();
127
128     authStorageService = TestBed.inject(AuthStorageService);
129     spyOn(authStorageService, 'getUsername').and.returnValue('someUser');
130
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();
137   });
138
139   it('should create', () => {
140     expect(component).toBeTruthy();
141     expect(_.isArray(component.rules)).toBeTruthy();
142   });
143
144   it('should have set the logged in user name as creator', () => {
145     expect(component.form.getValue('createdBy')).toBe('someUser');
146   });
147
148   it('should call disablePrometheusConfig on error calling getRules', () => {
149     spyOn(prometheusService, 'disablePrometheusConfig');
150     rulesSpy.and.callFake(() => throwError({}));
151     callInit();
152     expect(component.rules).toEqual([]);
153     expect(prometheusService.disablePrometheusConfig).toHaveBeenCalled();
154   });
155
156   it('should remind user if prometheus is not set when it is not configured', () => {
157     ifPrometheusSpy.and.callFake((_x: any, fn: Function) => fn());
158     callInit();
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',
163       undefined,
164       undefined,
165       'Prometheus'
166     );
167   });
168
169   describe('redirect not allowed users', () => {
170     let prometheusPermissions: Permission;
171     let navigateSpy: jasmine.Spy;
172
173     const expectRedirect = (action: string, redirected: boolean) => {
174       changeAction(action);
175       expect(router.navigate).toHaveBeenCalledTimes(redirected ? 1 : 0);
176       if (redirected) {
177         expect(router.navigate).toHaveBeenCalledWith(['/404']);
178       }
179       navigateSpy.calls.reset();
180     };
181
182     beforeEach(() => {
183       navigateSpy = spyOn(router, 'navigate').and.stub();
184       spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
185         prometheus: prometheusPermissions
186       }));
187     });
188
189     it('redirects to 404 if not allowed', () => {
190       prometheusPermissions = new Permission(['delete', 'read']);
191       expectRedirect('add', true);
192       expectRedirect('alertAdd', true);
193     });
194
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);
200     });
201
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);
207     });
208
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);
214     });
215
216     it('does not redirect if user has minimum permissions to update silences', () => {
217       prometheusPermissions = new Permission(['update', 'read']);
218       expectRedirect('edit', false);
219     });
220   });
221
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);
228     };
229
230     beforeEach(() => {
231       spyOn(prometheusService, 'getSilences').and.callFake((p) =>
232         of([prometheus.createSilence(p.id)])
233       );
234     });
235
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({
240         comment: null,
241         createdBy: 'someUser',
242         duration: '2h',
243         startsAt: baseTime,
244         endsAt: '2022-02-22 02:00'
245       });
246     });
247
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}`,
255         duration: '1d',
256         startsAt: '2022-02-22 22:22',
257         endsAt: '2022-02-23 22:22'
258       });
259       expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
260     });
261
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}`,
269         duration: '2h',
270         startsAt: baseTime,
271         endsAt: '2022-02-22 02:00'
272       });
273       expect(component.matchers).toEqual([createMatcher('job', 'someJob', true)]);
274     });
275
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)
286       ]);
287       expect(component.matcherMatch).toEqual({
288         cssClass: 'has-success',
289         status: 'Matches 1 rule with 1 active alert.'
290       });
291     });
292   });
293
294   describe('time', () => {
295     const changeEndDate = (text: string) => component.form.patchValue({ endsAt: text });
296     const changeStartDate = (text: string) => component.form.patchValue({ startsAt: text });
297
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');
302     });
303
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');
309
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');
313       }));
314
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');
319       }));
320
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');
326       }));
327     });
328
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');
335       });
336     });
337
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);
343       }));
344
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');
349       }));
350
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);
356       }));
357     });
358   });
359
360   it('should have a creator field', () => {
361     formHelper.expectValid('createdBy');
362     formHelper.expectErrorChange('createdBy', '', 'required');
363     formHelper.expectValidChange('createdBy', 'Mighty FSM');
364   });
365
366   it('should have a comment field', () => {
367     formHelper.expectError('comment', 'required');
368     formHelper.expectValidChange('comment', 'A pretty long comment');
369   });
370
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();
377   });
378
379   describe('matchers', () => {
380     const expectMatch = (helpText: string) => {
381       expect(fixtureH.getText('#match-state')).toBe(helpText);
382     };
383
384     it('should show the add matcher button', () => {
385       fixtureH.expectElementVisible('#add-matcher', true);
386       fixtureH.expectIdElementsVisible(
387         [
388           'matcher-name-0',
389           'matcher-value-0',
390           'matcher-isRegex-0',
391           'matcher-edit-0',
392           'matcher-delete-0'
393         ],
394         false
395       );
396       expectMatch(null);
397     });
398
399     it('should show added matcher', () => {
400       addMatcher('job', 'someJob', true);
401       fixtureH.expectIdElementsVisible(
402         [
403           'matcher-name-0',
404           'matcher-value-0',
405           'matcher-isRegex-0',
406           'matcher-edit-0',
407           'matcher-delete-0'
408         ],
409         true
410       );
411       expectMatch(null);
412     });
413
414     it('should show multiple matchers', () => {
415       addMatcher('severity', 'someSeverity', false);
416       addMatcher('alertname', 'alert0', false);
417       fixtureH.expectIdElementsVisible(
418         [
419           'matcher-name-0',
420           'matcher-value-0',
421           'matcher-isRegex-0',
422           'matcher-edit-0',
423           'matcher-delete-0',
424           'matcher-name-1',
425           'matcher-value-1',
426           'matcher-isRegex-1',
427           'matcher-edit-1',
428           'matcher-delete-1'
429         ],
430         true
431       );
432       expectMatch('Matches 1 rule with 1 active alert.');
433     });
434
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');
443       expectMatch(null);
444     });
445
446     it('should be able to edit a matcher', () => {
447       addMatcher('alertname', 'alert.*', true);
448       expectMatch(null);
449
450       const modalService = TestBed.inject(ModalService);
451       spyOn(modalService, 'show').and.callFake(() => {
452         return {
453           componentInstance: {
454             preFillControls: (matcher: any) => {
455               expect(matcher).toBe(component.matchers[0]);
456             },
457             submitAction: of({ name: 'alertname', value: 'alert0', isRegex: false })
458           }
459         };
460       });
461       fixtureH.clickElement('#matcher-edit-0');
462
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.');
467     });
468
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'],
476         false
477       );
478       expectMatch(null);
479     });
480
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.');
487     });
488
489     it('should show form as invalid if no matcher is set', () => {
490       expect(form.errors).toEqual({ matcherRequired: true });
491     });
492
493     it('should show form as valid if matcher was added', () => {
494       addMatcher('some name', 'some value', true);
495       expect(form.errors).toEqual(null);
496     });
497   });
498
499   describe('submit tests', () => {
500     const endsAt = '2022-02-22 02:00';
501     let silence: AlertmanagerSilence;
502     const silenceId = '50M3-10N6-1D';
503
504     const expectSuccessNotification = (titleStartsWith: string) =>
505       expect(notificationService.show).toHaveBeenCalledWith(
506         NotificationType.success,
507         `${titleStartsWith} silence ${silenceId}`,
508         undefined,
509         undefined,
510         'Prometheus'
511       );
512
513     const fillAndSubmit = () => {
514       ['createdBy', 'comment'].forEach((attr) => {
515         formHelper.setValue(attr, silence[attr]);
516       });
517       silence.matchers.forEach((matcher) =>
518         addMatcher(matcher.name, matcher.value, matcher.isRegex)
519       );
520       component.submit();
521     };
522
523     beforeEach(() => {
524       spyOn(prometheusService, 'setSilence').and.callFake(() => of({ body: { silenceId } }));
525       spyOn(router, 'navigate').and.stub();
526       silence = {
527         createdBy: 'some creator',
528         comment: 'some comment',
529         startsAt: moment(baseTime).toISOString(),
530         endsAt: moment(endsAt).toISOString(),
531         matchers: [
532           {
533             name: 'some attribute name',
534             value: 'some value',
535             isRegex: false
536           },
537           {
538             name: 'job',
539             value: 'node-exporter',
540             isRegex: false
541           },
542           {
543             name: 'instance',
544             value: 'localhost:9100',
545             isRegex: false
546           },
547           {
548             name: 'alertname',
549             value: 'load_0',
550             isRegex: false
551           }
552         ]
553       };
554     });
555
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();
562     // });
563
564     // it('should route back to previous tab on success', () => {
565     //   fillAndSubmit();
566     //   expect(form.valid).toBeTruthy();
567     //   expect(router.navigate).toHaveBeenCalledWith(['/monitoring'], { fragment: 'silences' });
568     // });
569
570     it('should create a silence', () => {
571       fillAndSubmit();
572       expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
573       expectSuccessNotification('Created');
574     });
575
576     it('should recreate a silence', () => {
577       component.recreate = true;
578       component.id = 'recreateId';
579       fillAndSubmit();
580       expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
581       expectSuccessNotification('Recreated');
582     });
583
584     it('should edit a silence', () => {
585       component.edit = true;
586       component.id = 'editId';
587       silence.id = component.id;
588       fillAndSubmit();
589       expect(prometheusService.setSilence).toHaveBeenCalledWith(silence);
590       expectSuccessNotification('Edited');
591     });
592   });
593 });