Fix PolicyInstanceComponent and add test coverage
[portal/nonrtric-controlpanel.git] / webapp-frontend / src / app / policy / policy-instance / policy-instance.component.spec.ts
1 /*-
2  * ========================LICENSE_START=================================
3  * O-RAN-SC
4  * %%
5  * Copyright (C) 2020 Nordix Foundation
6  * %%
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ========================LICENSE_END===================================
19  */
20
21 import { HarnessLoader } from "@angular/cdk/testing";
22 import { TestbedHarnessEnvironment } from "@angular/cdk/testing/testbed";
23 import { HttpResponse } from "@angular/common/http";
24 import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
25 import { ComponentFixture, TestBed } from "@angular/core/testing";
26 import { FormsModule, ReactiveFormsModule } from "@angular/forms";
27 import { MatButtonHarness } from "@angular/material/button/testing";
28 import { MatDialog } from "@angular/material/dialog";
29 import { MatIconModule } from "@angular/material/icon";
30 import { MatInputHarness } from "@angular/material/input/testing";
31 import { MatSortModule } from "@angular/material/sort";
32 import { MatSortHarness } from "@angular/material/sort/testing";
33 import { MatTableModule } from "@angular/material/table";
34 import { MatTableHarness } from "@angular/material/table/testing";
35 import { By } from "@angular/platform-browser";
36 import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
37 import {
38   PolicyInstance,
39   PolicyInstances,
40   PolicyStatus,
41   PolicyTypeSchema,
42 } from "@app/interfaces/policy.types";
43 import { PolicyService } from "@app/services/policy/policy.service";
44 import { ConfirmDialogService } from "@app/services/ui/confirm-dialog.service";
45 import { NotificationService } from "@app/services/ui/notification.service";
46 import { UiService } from "@app/services/ui/ui.service";
47 import { ToastrModule } from "ngx-toastr";
48 import { Observable, of } from "rxjs";
49 import { PolicyInstanceDialogComponent } from "../policy-instance-dialog/policy-instance-dialog.component";
50 import { PolicyInstanceComponent } from "./policy-instance.component";
51
52 const lastModifiedTime = "2021-01-26T13:15:11.895297Z";
53 describe("PolicyInstanceComponent", () => {
54   let hostComponent: PolicyInstanceComponentHostComponent;
55   let componentUnderTest: PolicyInstanceComponent;
56   let hostFixture: ComponentFixture<PolicyInstanceComponentHostComponent>;
57   let loader: HarnessLoader;
58   let policyServiceSpy: jasmine.SpyObj<PolicyService>;
59   let dialogSpy: jasmine.SpyObj<MatDialog>;
60   let notificationServiceSpy: jasmine.SpyObj<NotificationService>;
61   let confirmServiceSpy: jasmine.SpyObj<ConfirmDialogService>;
62
63   const policyInstances = {
64     policy_ids: ["policy1", "policy2"],
65   } as PolicyInstances;
66   const policyTypeSchema = JSON.parse(
67     '{"title": "1", "description": "Type 1 policy type"}'
68   );
69   const policy1 = {
70     policy_id: "policy1",
71     policy_data: "{}",
72     ric_id: "1",
73     service_id: "service",
74     lastModified: "Now",
75   } as PolicyInstance;
76   const policy2 = {
77     policy_id: "policy2",
78     policy_data: "{}",
79     ric_id: "2",
80     service_id: "service",
81     lastModified: "Now",
82   } as PolicyInstance;
83   const policy1Status = {
84     last_modified: lastModifiedTime,
85   } as PolicyStatus;
86   const policy2Status = {
87     last_modified: lastModifiedTime,
88   } as PolicyStatus;
89
90   const policyIdToInstanceMap = {
91     policy1: policy1,
92     policy2: policy2,
93   };
94   const policyIdToStatusMap = {
95     policy1: policy1Status,
96     policy2: policy2Status,
97   };
98
99   @Component({
100     selector: "policy-instance-compnent-host-component",
101     template:
102       "<nrcp-policy-instance [policyTypeSchema]=policyType></nrcp-policy-instance>",
103   })
104   class PolicyInstanceComponentHostComponent {
105     policyType = {
106       id: "type1",
107       name: "1",
108       schemaObject: policyTypeSchema,
109     } as PolicyTypeSchema;
110   }
111
112   beforeEach(async () => {
113     policyServiceSpy = jasmine.createSpyObj("PolicyService", [
114       "getPolicyInstancesByType",
115       "getPolicyInstance",
116       "getPolicyStatus",
117       "deletePolicy",
118     ]);
119     policyServiceSpy.getPolicyInstancesByType.and.returnValue(
120       of(policyInstances)
121     );
122     policyServiceSpy.getPolicyInstance.and.callFake(function (
123       policyId: string
124     ) {
125       return of(policyIdToInstanceMap[policyId]);
126     });
127     policyServiceSpy.getPolicyStatus.and.callFake(function (policyId: string) {
128       return of(policyIdToStatusMap[policyId]);
129     });
130
131     dialogSpy = jasmine.createSpyObj("MatDialog", ["open"]);
132     notificationServiceSpy = jasmine.createSpyObj("NotificationService", [
133       "success",
134       "warn",
135     ]);
136     confirmServiceSpy = jasmine.createSpyObj("ConfirmDialogService", [
137       "openConfirmDialog",
138     ]);
139
140     await TestBed.configureTestingModule({
141       imports: [
142         BrowserAnimationsModule,
143         FormsModule,
144         MatIconModule,
145         MatSortModule,
146         MatTableModule,
147         ReactiveFormsModule,
148         ToastrModule.forRoot(),
149       ],
150       schemas: [CUSTOM_ELEMENTS_SCHEMA],
151       declarations: [
152         PolicyInstanceComponent,
153         PolicyInstanceComponentHostComponent,
154       ],
155       providers: [
156         { provide: PolicyService, useValue: policyServiceSpy },
157         { provide: MatDialog, useValue: dialogSpy },
158         { provide: NotificationService, useValue: notificationServiceSpy },
159         { provide: ConfirmDialogService, useValue: confirmServiceSpy },
160         UiService,
161       ],
162     }).compileComponents();
163
164     hostFixture = TestBed.createComponent(PolicyInstanceComponentHostComponent);
165     hostComponent = hostFixture.componentInstance;
166     componentUnderTest = hostFixture.debugElement.query(
167       By.directive(PolicyInstanceComponent)
168     ).componentInstance;
169     hostFixture.detectChanges();
170     loader = TestbedHarnessEnvironment.loader(hostFixture);
171   });
172
173   it("should create", () => {
174     expect(hostComponent).toBeTruthy();
175
176     expect(componentUnderTest).toBeTruthy();
177   });
178
179   it("should set correct dark mode from UIService", () => {
180     const uiService: UiService = TestBed.inject(UiService);
181     expect(componentUnderTest.darkMode).toBeTruthy();
182
183     uiService.darkModeState.next(false);
184     hostFixture.detectChanges();
185     expect(componentUnderTest.darkMode).toBeFalsy();
186   });
187
188   it("should contain number of instances heading and value, create and refresh buttons, and policies table", async () => {
189     const instancesHeading = hostFixture.debugElement.nativeElement.querySelector(
190       "div"
191     );
192     expect(instancesHeading.innerText).toContain("Number of instances: 2");
193
194     const createButton: MatButtonHarness = await loader.getHarness(
195       MatButtonHarness.with({ selector: "#createButton" })
196     );
197     expect(createButton).toBeTruthy();
198     const createIcon = hostFixture.debugElement.nativeElement.querySelector(
199       "#createIcon"
200     );
201     expect(createIcon.innerText).toContain("add_box");
202
203     const refreshButton: MatButtonHarness = await loader.getHarness(
204       MatButtonHarness.with({ selector: "#refreshButton" })
205     );
206     expect(refreshButton).toBeTruthy();
207     const refreshIcon = hostFixture.debugElement.nativeElement.querySelector(
208       "#refreshIcon"
209     );
210     expect(refreshIcon.innerText).toContain("refresh");
211
212     const policiesTable = await loader.getHarness(
213       MatTableHarness.with({ selector: "#policiesTable" })
214     );
215     expect(policiesTable).toBeTruthy();
216   });
217
218   it("should open dialog to create policy and refresh policies after successful creation", async () => {
219     const dialogRefSpy = setupDialogRefSpy();
220     dialogSpy.open.and.returnValue(dialogRefSpy);
221
222     spyOn(componentUnderTest, "getPolicyInstances");
223
224     const createButton: MatButtonHarness = await loader.getHarness(
225       MatButtonHarness.with({ selector: "#createButton" })
226     );
227     await createButton.click();
228
229     expect(dialogSpy.open).toHaveBeenCalledWith(PolicyInstanceDialogComponent, {
230       maxWidth: "1200px",
231       maxHeight: "900px",
232       width: "900px",
233       role: "dialog",
234       disableClose: false,
235       panelClass: "dark-theme",
236       data: {
237         createSchema: policyTypeSchema,
238         instanceId: null,
239         instanceJson: null,
240         name: "1",
241         ric: null,
242       },
243     });
244     expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled();
245   });
246
247   it("should open dialog to edit policy and refresh policies after successful update", async () => {
248     const dialogRefSpy = setupDialogRefSpy();
249     dialogSpy.open.and.returnValue(dialogRefSpy);
250
251     spyOn(componentUnderTest, "getPolicyInstances");
252
253     const editButton: MatButtonHarness = await loader.getHarness(
254       MatButtonHarness.with({ selector: "#policy1EditButton" })
255     );
256     await editButton.click();
257
258     expect(dialogSpy.open).toHaveBeenCalledWith(PolicyInstanceDialogComponent, {
259       maxWidth: "1200px",
260       maxHeight: "900px",
261       width: "900px",
262       role: "dialog",
263       disableClose: false,
264       panelClass: "dark-theme",
265       data: {
266         createSchema: policyTypeSchema,
267         instanceId: "policy1",
268         instanceJson: "{}",
269         name: "1",
270         ric: "1",
271       },
272     });
273     expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled();
274   });
275
276   it("should open dialog to edit policy and not refresh policies when dialog closed wihtout submit", async () => {
277     const dialogRefSpy = setupDialogRefSpy(false);
278     dialogSpy.open.and.returnValue(dialogRefSpy);
279
280     spyOn(componentUnderTest, "getPolicyInstances");
281
282     const editButton: MatButtonHarness = await loader.getHarness(
283       MatButtonHarness.with({ selector: "#policy1EditButton" })
284     );
285     await editButton.click();
286
287     expect(componentUnderTest.getPolicyInstances).not.toHaveBeenCalled();
288   });
289
290   it("should open instance dialog when clicking in any policy cell in table", async () => {
291     spyOn(componentUnderTest, "modifyInstance");
292
293     const policiesTable = await loader.getHarness(
294       MatTableHarness.with({ selector: "#policiesTable" })
295     );
296     const firstRow = (await policiesTable.getRows())[0];
297     const idCell = (await firstRow.getCells())[0];
298     (await idCell.host()).click();
299     const ownerCell = (await firstRow.getCells())[1];
300     (await ownerCell.host()).click();
301     const serviceCell = (await firstRow.getCells())[2];
302     (await serviceCell.host()).click();
303     const lastModifiedCell = (await firstRow.getCells())[3];
304     (await lastModifiedCell.host()).click();
305
306     // Totally unnecessary call just to make the bloody framework count the number of calls to the spy correctly!
307     await policiesTable.getRows();
308
309     expect(componentUnderTest.modifyInstance).toHaveBeenCalledTimes(4);
310   });
311
312   it("should open dialog asking for delete and delete when ok response and refresh table afterwards", async () => {
313     const dialogRefSpy = setupDialogRefSpy();
314     confirmServiceSpy.openConfirmDialog.and.returnValue(dialogRefSpy);
315     const createResponse = { status: 204 } as HttpResponse<Object>;
316     policyServiceSpy.deletePolicy.and.returnValue(of(createResponse));
317
318     spyOn(componentUnderTest, "getPolicyInstances");
319     const deleteButton: MatButtonHarness = await loader.getHarness(
320       MatButtonHarness.with({ selector: "#policy1DeleteButton" })
321     );
322     await deleteButton.click();
323
324     expect(confirmServiceSpy.openConfirmDialog).toHaveBeenCalledWith(
325       "Are you sure you want to delete this policy instance?"
326     );
327     expect(policyServiceSpy.deletePolicy).toHaveBeenCalledWith("policy1");
328     expect(notificationServiceSpy.success).toHaveBeenCalledWith(
329       "Delete succeeded!"
330     );
331     expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled();
332   });
333
334   it("should open dialog asking for delete and not delete whith Cancel as response", async () => {
335     const dialogRefSpy = setupDialogRefSpy(false);
336     confirmServiceSpy.openConfirmDialog.and.returnValue(dialogRefSpy);
337
338     const deleteButton: MatButtonHarness = await loader.getHarness(
339       MatButtonHarness.with({ selector: "#policy1DeleteButton" })
340     );
341     await deleteButton.click();
342
343     expect(policyServiceSpy.deletePolicy).not.toHaveBeenCalled();
344   });
345
346   it("should refresh table", async () => {
347     spyOn(componentUnderTest, "getPolicyInstances");
348
349     const refreshButton: MatButtonHarness = await loader.getHarness(
350       MatButtonHarness.with({ selector: "#refreshButton" })
351     );
352     await refreshButton.click();
353
354     expect(componentUnderTest.getPolicyInstances).toHaveBeenCalled();
355   });
356
357   describe("#policiesTable", () => {
358     const expectedPolicy1Row = {
359       instanceId: "policy1",
360       ric: "1",
361       service: "service",
362       lastModified: toLocalTime(lastModifiedTime),
363       action: "editdelete",
364     };
365
366     it("should contain correct headings", async () => {
367       const policiesTable = await loader.getHarness(
368         MatTableHarness.with({ selector: "#policiesTable" })
369       );
370       const headerRow = (await policiesTable.getHeaderRows())[0];
371       const headers = await headerRow.getCellTextByColumnName();
372
373       expect(headers).toEqual({
374         instanceId: "Instance",
375         ric: "Target",
376         service: "Owner",
377         lastModified: "Last modified",
378         action: "Action",
379       });
380     });
381
382     it("should contain data after initialization", async () => {
383       const expectedJobRows = [
384         expectedPolicy1Row,
385         {
386           instanceId: "policy2",
387           ric: "2",
388           service: "service",
389           lastModified: toLocalTime(lastModifiedTime),
390           action: "editdelete",
391         },
392       ];
393       const policiesTable = await loader.getHarness(
394         MatTableHarness.with({ selector: "#policiesTable" })
395       );
396       const policyRows = await policiesTable.getRows();
397       expect(policyRows.length).toEqual(2);
398       policyRows.forEach((row) => {
399         row.getCellTextByColumnName().then((values) => {
400           expect(expectedJobRows).toContain(jasmine.objectContaining(values));
401         });
402       });
403     });
404
405     it("should have filtering for all four policy data headings", async () => {
406       const policiesTable = await loader.getHarness(
407         MatTableHarness.with({ selector: "#policiesTable" })
408       );
409
410       const idFilterInput = await loader.getHarness(
411         MatInputHarness.with({ selector: "#policyInstanceIdFilter" })
412       );
413       await idFilterInput.setValue("1");
414       const policyRows = await policiesTable.getRows();
415       expect(policyRows.length).toEqual(1);
416       expect(await policyRows[0].getCellTextByColumnName()).toEqual(
417         expectedPolicy1Row
418       );
419
420       const targetFilterInput = await loader.getHarness(
421         MatInputHarness.with({ selector: "#policyInstanceTargetFilter" })
422       );
423       expect(targetFilterInput).toBeTruthy();
424
425       const ownerFilterInput = await loader.getHarness(
426         MatInputHarness.with({ selector: "#policyInstanceOwnerFilter" })
427       );
428       expect(ownerFilterInput).toBeTruthy();
429
430       const lastModifiedFilterInput = await loader.getHarness(
431         MatInputHarness.with({ selector: "#policyInstanceLastModifiedFilter" })
432       );
433       expect(lastModifiedFilterInput).toBeTruthy();
434     });
435
436     it("should not sort when click in filter inputs", async () => {
437       spyOn(componentUnderTest, "stopSort").and.callThrough();
438
439       const idFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector(
440         "#idSortStop"
441       );
442       idFilterInputDiv.click();
443
444       const targetFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector(
445         "#targetSortStop"
446       );
447       targetFilterInputDiv.click();
448
449       const ownerFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector(
450         "#ownerSortStop"
451       );
452       ownerFilterInputDiv.click();
453
454       const lastModifiedFilterInputDiv = hostFixture.debugElement.nativeElement.querySelector(
455         "#lastModifiedSortStop"
456       );
457       lastModifiedFilterInputDiv.click();
458
459       expect(componentUnderTest.stopSort).toHaveBeenCalledTimes(4);
460
461       const eventSpy = jasmine.createSpyObj("any", ["stopPropagation"]);
462       componentUnderTest.stopSort(eventSpy);
463       expect(eventSpy.stopPropagation).toHaveBeenCalled();
464     });
465
466     describe("#sorting", () => {
467       it("should verify sort functionality on the table", async () => {
468         const sort = await loader.getHarness(MatSortHarness);
469         const headers = await sort.getSortHeaders({ sortDirection: "" });
470         expect(headers.length).toBe(4);
471
472         await headers[0].click();
473         expect(await headers[0].isActive()).toBe(true);
474         expect(await headers[0].getSortDirection()).toBe("asc");
475
476         await headers[0].click();
477         expect(await headers[0].getSortDirection()).toBe("desc");
478       });
479
480       it("should sort table asc and desc by first header", async () => {
481         const sort = await loader.getHarness(MatSortHarness);
482         const policyTable = await loader.getHarness(
483           MatTableHarness.with({ selector: "#policiesTable" })
484         );
485         const firstHeader = (await sort.getSortHeaders())[0];
486         expect(await firstHeader.getSortDirection()).toBe("");
487
488         await firstHeader.click();
489         expect(await firstHeader.getSortDirection()).toBe("asc");
490         let policyRows = await policyTable.getRows();
491         expect(await policyRows[0].getCellTextByColumnName()).toEqual(
492           expectedPolicy1Row
493         );
494
495         await firstHeader.click();
496         expect(await firstHeader.getSortDirection()).toBe("desc");
497         policyRows = await policyTable.getRows();
498         expect(
499           await policyRows[policyRows.length - 1].getCellTextByColumnName()
500         ).toEqual(expectedPolicy1Row);
501       });
502
503       it("should sort table asc and desc by second header", async () => {
504         const sort = await loader.getHarness(MatSortHarness);
505         const jobsTable = await loader.getHarness(
506           MatTableHarness.with({ selector: "#policiesTable" })
507         );
508         const firstHeader = (await sort.getSortHeaders())[1];
509         expect(await firstHeader.getSortDirection()).toBe("");
510
511         await firstHeader.click();
512         expect(await firstHeader.getSortDirection()).toBe("asc");
513         let policyRows = await jobsTable.getRows();
514         policyRows = await jobsTable.getRows();
515         expect(await policyRows[0].getCellTextByColumnName()).toEqual(
516           expectedPolicy1Row
517         );
518
519         await firstHeader.click();
520         expect(await firstHeader.getSortDirection()).toBe("desc");
521         policyRows = await jobsTable.getRows();
522         expect(
523           await policyRows[policyRows.length - 1].getCellTextByColumnName()
524         ).toEqual(expectedPolicy1Row);
525       });
526
527       it("should sort table asc and desc by third header", async () => {
528         const sort = await loader.getHarness(MatSortHarness);
529         const jobsTable = await loader.getHarness(
530           MatTableHarness.with({ selector: "#policiesTable" })
531         );
532         const firstHeader = (await sort.getSortHeaders())[2];
533         expect(await firstHeader.getSortDirection()).toBe("");
534
535         await firstHeader.click();
536         expect(await firstHeader.getSortDirection()).toBe("asc");
537         let policyRows = await jobsTable.getRows();
538         policyRows = await jobsTable.getRows();
539         expect(await policyRows[0].getCellTextByColumnName()).toEqual(
540           expectedPolicy1Row
541         );
542
543         await firstHeader.click();
544         expect(await firstHeader.getSortDirection()).toBe("desc");
545         policyRows = await jobsTable.getRows();
546         expect(
547           await policyRows[policyRows.length - 1].getCellTextByColumnName()
548         ).toEqual(expectedPolicy1Row);
549       });
550
551       it("should sort table asc and desc by fourth header", async () => {
552         const sort = await loader.getHarness(MatSortHarness);
553         const jobsTable = await loader.getHarness(
554           MatTableHarness.with({ selector: "#policiesTable" })
555         );
556         const firstHeader = (await sort.getSortHeaders())[3];
557         expect(await firstHeader.getSortDirection()).toBe("");
558
559         await firstHeader.click();
560         expect(await firstHeader.getSortDirection()).toBe("asc");
561         let policyRows = await jobsTable.getRows();
562         policyRows = await jobsTable.getRows();
563         expect(await policyRows[0].getCellTextByColumnName()).toEqual(
564           expectedPolicy1Row
565         );
566
567         await firstHeader.click();
568         expect(await firstHeader.getSortDirection()).toBe("desc");
569         policyRows = await jobsTable.getRows();
570         expect(
571           await policyRows[policyRows.length - 1].getCellTextByColumnName()
572         ).toEqual(expectedPolicy1Row);
573       });
574     });
575   });
576 });
577
578 function setupDialogRefSpy(returnValue: boolean = true) {
579   const afterClosedObservable = new Observable((observer) => {
580     observer.next(returnValue);
581   });
582
583   const dialogRefSpy = jasmine.createSpyObj("MatDialogRef", ["afterClosed"]);
584   dialogRefSpy.afterClosed.and.returnValue(afterClosedObservable);
585   return dialogRefSpy;
586 }
587
588 function toLocalTime(utcTime: string): string {
589   const date = new Date(utcTime);
590   const toutc = date.toUTCString();
591   return new Date(toutc + " UTC").toLocaleString();
592 }