diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 6bb2029b1..61444c381 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -17,5 +17,5 @@ RUN if [ -n "$VERSION" ]; then \ RUN ng build --configuration=production FROM public.ecr.aws/docker/library/nginx:alpine -COPY --from=builder /app/dist/dissendium-v0/ /usr/share/nginx/html/ +COPY --from=builder /app/dist/rocketadmin/ /usr/share/nginx/html/ COPY nginx/default.conf.template /etc/nginx/templates/ diff --git a/frontend/angular.json b/frontend/angular.json index ca79e6428..d17ef10f6 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -3,7 +3,7 @@ "version": 1, "newProjectRoot": "projects", "projects": { - "dissendium-v0": { + "rocketadmin": { "projectType": "application", "schematics": {}, "root": "", @@ -14,7 +14,7 @@ "builder": "@angular/build:application", "options": { "outputPath": { - "base": "dist/dissendium-v0" + "base": "dist/rocketadmin" }, "index": "src/index.html", "polyfills": ["src/polyfills.ts"], @@ -87,7 +87,7 @@ "fileReplacements": [ { "replace": "src/environments/environment.ts", - "with": "src/environments/environment.saas-prod.ts" + "with": "src/environments/environment.saas.ts" } ] }, @@ -126,32 +126,32 @@ "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "buildTarget": "dissendium-v0:build", + "buildTarget": "rocketadmin:build", "host": "127.0.0.1", "proxyConfig": "src/proxy.conf.json" }, "configurations": { "production": { - "buildTarget": "dissendium-v0:build:production" + "buildTarget": "rocketadmin:build:production" }, "saas": { - "buildTarget": "dissendium-v0:build:saas" + "buildTarget": "rocketadmin:build:saas" }, "development": { - "buildTarget": "dissendium-v0:build:development" + "buildTarget": "rocketadmin:build:development" } } }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "buildTarget": "dissendium-v0:build" + "buildTarget": "rocketadmin:build" } }, "test": { "builder": "@angular/build:unit-test", "options": { - "buildTarget": "dissendium-v0:build", + "buildTarget": "rocketadmin:build", "runner": "vitest", "tsConfig": "tsconfig.spec.json", "setupFiles": ["src/test-setup.ts"], diff --git a/frontend/package.json b/frontend/package.json index 00a3f4e56..2cb9d381d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "dissendium-v0", + "name": "rocketadmin", "version": "1.0.0", "scripts": { "ng": "ng", @@ -9,7 +9,7 @@ "test:ci": "ng test --no-watch", "lint": "ng lint", "e2e": "ng e2e", - "analyze": "webpack-bundle-analyzer dist/dissendium-v0/stats.json", + "analyze": "webpack-bundle-analyzer dist/rocketadmin/stats.json", "build:stats": "node scripts/update-version.js && ng build --stats-json", "update-version": "node scripts/update-version.js" }, diff --git a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html index e3d465600..299490e5e 100644 --- a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html +++ b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.html @@ -70,10 +70,16 @@

Add member to {{company.name}} company

+ + diff --git a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts index 5e295085a..04f75f9c0 100644 --- a/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts +++ b/frontend/src/app/components/company/invite-member-dialog/invite-member-dialog.component.ts @@ -1,78 +1,117 @@ -import { CompanyMemberRole } from 'src/app/models/company'; -import { CompanyService } from 'src/app/services/company.service'; -import { Component, Inject } from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; +import { NgForOf, NgIf } from '@angular/common'; +import { Component, Inject, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatDialogModule, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; -import { MatButtonModule } from '@angular/material/button'; +import { MatMenuModule } from '@angular/material/menu'; import { MatSelectModule } from '@angular/material/select'; -import { NgForOf, NgIf } from '@angular/common'; +import { Angulartics2 } from 'angulartics2'; import { EmailValidationDirective } from 'src/app/directives/emailValidator.directive'; -import { MatMenuModule } from '@angular/material/menu'; -import { MatIconModule } from '@angular/material/icon'; +import { CompanyMemberRole } from 'src/app/models/company'; +import { CompanyService } from 'src/app/services/company.service'; +import { environment } from 'src/environments/environment'; +import { TurnstileComponent } from '../../ui-components/turnstile/turnstile.component'; @Component({ - selector: 'app-invite-member-dialog', - templateUrl: './invite-member-dialog.component.html', - styleUrls: ['./invite-member-dialog.component.css'], - standalone: true, - imports: [ - NgIf, - NgForOf, - FormsModule, - MatDialogModule, - MatFormFieldModule, - MatInputModule, - MatSelectModule, - MatButtonModule, - MatMenuModule, - MatIconModule, - EmailValidationDirective - ] + selector: 'app-invite-member-dialog', + templateUrl: './invite-member-dialog.component.html', + styleUrls: ['./invite-member-dialog.component.css'], + standalone: true, + imports: [ + NgIf, + NgForOf, + FormsModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatButtonModule, + MatMenuModule, + MatIconModule, + EmailValidationDirective, + TurnstileComponent, + ], }) export class InviteMemberDialogComponent { - CompanyMemberRole = CompanyMemberRole; + @ViewChild(TurnstileComponent) turnstileWidget: TurnstileComponent; + + CompanyMemberRole = CompanyMemberRole; + + public isSaas = (environment as any).saas; + public turnstileToken: string | null = null; + public companyMemberEmail: string; + public companyMemberRole: CompanyMemberRole = CompanyMemberRole.Member; + public submitting: boolean = false; + public companyUsersGroup: string = null; + public groups: { + title: string; + groups: object[]; + }[] = []; + + public companyRolesName = { + ADMIN: 'Account Owner', + DB_ADMIN: 'System Admin', + USER: 'Member', + }; + + constructor( + @Inject(MAT_DIALOG_DATA) public company: any, + public dialogRef: MatDialogRef, + private _company: CompanyService, + private angulartics2: Angulartics2, + ) {} + + ngOnInit(): void { + this.groups = this.company.connections.sort((a, b) => a.isTestConnection - b.isTestConnection); + } - public companyMemberEmail: string; - public companyMemberRole: CompanyMemberRole = CompanyMemberRole.Member; - public submitting: boolean = false; - public companyUsersGroup: string = null; - public groups: { - title: string, - groups: object[] - }[] = []; + addCompanyMember() { + this.submitting = true; + this._company + .inviteCompanyMember( + this.company.id, + this.companyUsersGroup, + this.companyMemberEmail, + this.companyMemberRole, + this.isSaas ? this.turnstileToken : null, + ) + .subscribe( + () => { + this.angulartics2.eventTrack.next({ + action: 'Company: member is invited successfully', + }); - public companyRolesName = { - 'ADMIN': 'Account Owner', - 'DB_ADMIN': 'System Admin', - 'USER': 'Member' - } + this.submitting = false; + this.dialogRef.close(); + }, + () => { + this._resetTurnstile(); + }, + () => { + this.submitting = false; + }, + ); + } - constructor( - @Inject(MAT_DIALOG_DATA) public company: any, - public dialogRef: MatDialogRef, - private _company: CompanyService, - private angulartics2: Angulartics2, - ) { } + onTurnstileToken(token: string) { + this.turnstileToken = token; + } - ngOnInit(): void { - this.groups = this.company.connections.sort((a, b) => a.isTestConnection - b.isTestConnection); - } + onTurnstileError() { + this.turnstileToken = null; + } - addCompanyMember() { - this.submitting = true; - this._company.inviteCompanyMember(this.company.id, this.companyUsersGroup, this.companyMemberEmail, this.companyMemberRole) - .subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Company: member is invited successfully', - }); + onTurnstileExpired() { + this.turnstileToken = null; + } - this.submitting = false; - this.dialogRef.close(); - }, - () => {}, - () => {this.submitting = false}); - } + private _resetTurnstile(): void { + if (this.isSaas && this.turnstileWidget) { + this.turnstileWidget.reset(); + this.turnstileToken = null; + } + } } diff --git a/frontend/src/app/components/connect-db/db-connection-confirm-dialog/db-connection-confirm-dialog.component.spec.ts b/frontend/src/app/components/connect-db/db-connection-confirm-dialog/db-connection-confirm-dialog.component.spec.ts index d98b14b7b..f2137a013 100644 --- a/frontend/src/app/components/connect-db/db-connection-confirm-dialog/db-connection-confirm-dialog.component.spec.ts +++ b/frontend/src/app/components/connect-db/db-connection-confirm-dialog/db-connection-confirm-dialog.component.spec.ts @@ -1,89 +1,88 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; - -import { Angulartics2Module } from 'angulartics2'; -import { ConnectionsService } from 'src/app/services/connections.service'; -import { DbConnectionConfirmDialogComponent } from './db-connection-confirm-dialog.component'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { provideRouter, Router } from '@angular/router'; +import { Angulartics2Module } from 'angulartics2'; import { of } from 'rxjs'; -import { provideHttpClient } from '@angular/common/http'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { DbConnectionConfirmDialogComponent } from './db-connection-confirm-dialog.component'; describe('DbConnectionConfirmDialogComponent', () => { - let component: DbConnectionConfirmDialogComponent; - let fixture: ComponentFixture; + let component: DbConnectionConfirmDialogComponent; + let fixture: ComponentFixture; - let routerSpy; - let fakeConnectionsService = { - updateConnection: vi.fn(), - createConnection: vi.fn() - }; + let routerSpy; + let fakeConnectionsService = { + updateConnection: vi.fn(), + createConnection: vi.fn(), + }; - beforeEach(async (): Promise => { - routerSpy = {navigate: vi.fn()}; + beforeEach(async (): Promise => { + routerSpy = { navigate: vi.fn() }; - await TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - MatDialogModule, - Angulartics2Module.forRoot(), - DbConnectionConfirmDialogComponent - ], - providers: [ - provideHttpClient(), - provideRouter([]), - { provide: MAT_DIALOG_DATA, useValue: { - dbCreds: { - id: '12345678' - } - } }, - { provide: MatDialogRef, useValue: {} }, - { provide: Router, useValue: routerSpy }, - { - provide: ConnectionsService, - useValue: fakeConnectionsService - } - ], - }).compileComponents(); - }); + await TestBed.configureTestingModule({ + imports: [MatSnackBarModule, MatDialogModule, Angulartics2Module.forRoot(), DbConnectionConfirmDialogComponent], + providers: [ + provideHttpClient(), + provideRouter([]), + { + provide: MAT_DIALOG_DATA, + useValue: { + dbCreds: { + id: '12345678', + }, + }, + }, + { provide: MatDialogRef, useValue: {} }, + { provide: Router, useValue: routerSpy }, + { + provide: ConnectionsService, + useValue: fakeConnectionsService, + }, + ], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(DbConnectionConfirmDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(DbConnectionConfirmDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should redirect on dashboard after connection edited', () => { - fakeConnectionsService.updateConnection.mockReturnValue(of(true)); - component.editConnection(); + it('should redirect on dashboard after connection edited', () => { + fakeConnectionsService.updateConnection.mockReturnValue(of(true)); + component.editConnection(); - expect(routerSpy.navigate).toHaveBeenCalledWith(['/dashboard/12345678']); - }); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/dashboard/12345678']); + }); - it('should stop submitting if editing connection completed', () => { - fakeConnectionsService.updateConnection.mockReturnValue(of(false)); - component.editConnection(); + it('should stop submitting if editing connection completed', () => { + fakeConnectionsService.updateConnection.mockReturnValue(of(false)); + component.editConnection(); - expect(component.submitting).toBe(false); - }); + expect(component.submitting).toBe(false); + }); - it('should redirect on dashboard after connection added', () => { - fakeConnectionsService.createConnection.mockReturnValue(of({ - id: '12345678' - })); - component.createConnection(); + it('should redirect on dashboard after connection added', () => { + fakeConnectionsService.createConnection.mockReturnValue( + of({ + id: '12345678', + }), + ); + component.createConnection(); - expect(routerSpy.navigate).toHaveBeenCalledWith(['/dashboard/12345678']); - }); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/dashboard/12345678']); + }); - it('should stop submitting if adding connection completed', () => { - fakeConnectionsService.createConnection.mockReturnValue(of(false)); - component.createConnection(); + it('should stop submitting if adding connection completed', () => { + fakeConnectionsService.createConnection.mockReturnValue(of(false)); + component.createConnection(); - expect(component.submitting).toBe(false); - }); + expect(component.submitting).toBe(false); + }); }); diff --git a/frontend/src/app/components/connection-settings/connection-settings.component.spec.ts b/frontend/src/app/components/connection-settings/connection-settings.component.spec.ts index 4fdf83f3d..e0fa08653 100644 --- a/frontend/src/app/components/connection-settings/connection-settings.component.spec.ts +++ b/frontend/src/app/components/connection-settings/connection-settings.component.spec.ts @@ -1,237 +1,242 @@ +import { provideHttpClient } from '@angular/common/http'; +import { forwardRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ConnectionSettings } from 'src/app/models/connection'; -import { ConnectionSettingsComponent } from './connection-settings.component'; -import { ConnectionsService } from 'src/app/services/connections.service'; import { MatDialogModule } from '@angular/material/dialog'; import { MatSelectModule } from '@angular/material/select'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; -import { TablesService } from 'src/app/services/tables.service'; -import { forwardRef } from '@angular/core'; -import { of } from 'rxjs'; -import { Angulartics2Module } from 'angulartics2'; -import { provideHttpClient } from '@angular/common/http'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { provideRouter } from '@angular/router'; +import { Angulartics2Module } from 'angulartics2'; +import { of } from 'rxjs'; +import { ConnectionSettings } from 'src/app/models/connection'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { TablesService } from 'src/app/services/tables.service'; +import { ConnectionSettingsComponent } from './connection-settings.component'; describe('ConnectionSettingsComponent', () => { - let component: ConnectionSettingsComponent; - let fixture: ComponentFixture; - let tablesService: TablesService; - let connectionsService: ConnectionsService; - - const mockTablesList = [ - { - "table": "customer", - "permissions": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - } - }, - { - "table": "Orders", - "permissions": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - }, - "display_name": "Created orders" - }, - { - "table": "product", - "permissions": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - } - } - ]; - - const mockConnectionSettings:ConnectionSettings = { - primary_color: "#1F5CB8", - secondary_color: "#F9D648", - logo_url: "https://www.shutterstock.com/image-vector/abstract-yellow-grunge-texture-isolated-260nw-1981157192.jpg", - company_name: "Such.Ukr.Lit", - hidden_tables: [ "writer_info", "address" ], - tables_audit: false, - default_showing_table: "customer" - }; - - const mockConnectionSettingsResponse = { - "id": "98a20557-6b38-46aa-b09b-d8a716421dd6", - "connectionId": "63f804e4-8588-4957-8d7f-655e2309fef7", - ...mockConnectionSettings - }; - - beforeEach(async () => { - const matSnackBarSpy = { open: vi.fn() }; - - await TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - MatDialogModule, - FormsModule, - MatSelectModule, - BrowserAnimationsModule, - Angulartics2Module.forRoot(), - ConnectionSettingsComponent - ], - providers: [ - provideHttpClient(), - provideRouter([]), - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ConnectionSettingsComponent), - multi: true - }, - { provide: MatSnackBar, useValue: matSnackBarSpy } - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(ConnectionSettingsComponent); - component = fixture.componentInstance; - tablesService = TestBed.inject(TablesService); - connectionsService = TestBed.inject(ConnectionsService); - vi.spyOn(connectionsService, 'currentConnectionID', 'get').mockReturnValue('12345678'); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should set table list', () => { - const fakeFetchTables = vi.spyOn(tablesService, 'fetchTables').mockReturnValue(of(mockTablesList)); - - component.ngOnInit(); - fixture.detectChanges(); - - expect(fakeFetchTables).toHaveBeenCalledWith('12345678', true); - expect(component.tablesList).toEqual([{ - "table": "customer", - "permissions": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - }, - "normalizedTableName": "Customers" - }, - { - "table": "Orders", - "permissions": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - }, - "display_name": "Created orders" - }, - { - "table": "product", - "permissions": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - }, - "normalizedTableName": "Products" - }]); - }); - - it('should show error if db is empty', () => { - const fakeFetchTables = vi.spyOn(tablesService, 'fetchTables').mockReturnValue(of([])); - - component.ngOnInit(); - fixture.detectChanges(); - - expect(fakeFetchTables).toHaveBeenCalledWith('12345678', true); - expect(component.noTablesError).toBe(true); - }); - - it('should set table settings if they are existed', () => { - const fakeGetSettings = vi.spyOn(connectionsService, 'getConnectionSettings').mockReturnValue(of(mockConnectionSettingsResponse)); - - component.getSettings(); - - expect(fakeGetSettings).toHaveBeenCalledWith('12345678'); - expect(component.connectionSettings).toEqual(mockConnectionSettingsResponse); - expect(component.isSettingsExist).toBe(true); - }); - - it('should set empty settings if they are not existed', () => { - const fakeGetSettings = vi.spyOn(connectionsService, 'getConnectionSettings').mockReturnValue(of(null)); - - component.getSettings(); - - expect(fakeGetSettings).toHaveBeenCalledWith('12345678'); - expect(component.connectionSettings).toEqual({ - hidden_tables:[], - default_showing_table: null, - primary_color: '', - secondary_color: '', - logo_url: '', - company_name: '', - tables_audit: true, - }); - expect(component.isSettingsExist).toBe(false); - }); - - it('should create settings', () => { - component.connectionSettings = mockConnectionSettings; - const fakeCreateSettings = vi.spyOn(connectionsService, 'createConnectionSettings').mockReturnValue(of()); - - component.createSettings(); - - expect(fakeCreateSettings).toHaveBeenCalledWith('12345678', { - primary_color: '#1F5CB8', - secondary_color: '#F9D648', - logo_url: 'https://www.shutterstock.com/image-vector/abstract-yellow-grunge-texture-isolated-260nw-1981157192.jpg', - company_name: 'Such.Ukr.Lit', - hidden_tables: [ "writer_info", "address" ], - default_showing_table: "customer", - tables_audit: false - }); - expect(component.submitting).toBe(false); - }); - - it('should update settings', () => { - component.connectionSettings = mockConnectionSettings; - const fakeUpdateSettings = vi.spyOn(connectionsService, 'updateConnectionSettings').mockReturnValue(of()); - - component.updateSettings(); - - expect(fakeUpdateSettings).toHaveBeenCalledWith('12345678', { - primary_color: '#1F5CB8', - secondary_color: '#F9D648', - logo_url: 'https://www.shutterstock.com/image-vector/abstract-yellow-grunge-texture-isolated-260nw-1981157192.jpg', - company_name: 'Such.Ukr.Lit', - hidden_tables: [ "writer_info", "address" ], - default_showing_table: "customer", - tables_audit: false - }); - expect(component.submitting).toBe(false); - }); - - it('should reset settings', () => { - const fakeDeleteSettings = vi.spyOn(connectionsService, 'deleteConnectionSettings').mockReturnValue(of()); - - component.resetSettings(); - - expect(fakeDeleteSettings).toHaveBeenCalledWith('12345678'); - expect(component.submitting).toBe(false); - }); + let component: ConnectionSettingsComponent; + let fixture: ComponentFixture; + let tablesService: TablesService; + let connectionsService: ConnectionsService; + + const mockTablesList = [ + { + table: 'customer', + permissions: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + }, + { + table: 'Orders', + permissions: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + display_name: 'Created orders', + }, + { + table: 'product', + permissions: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + }, + ]; + + const mockConnectionSettings: ConnectionSettings = { + primary_color: '#1F5CB8', + secondary_color: '#F9D648', + logo_url: 'https://www.shutterstock.com/image-vector/abstract-yellow-grunge-texture-isolated-260nw-1981157192.jpg', + company_name: 'Such.Ukr.Lit', + hidden_tables: ['writer_info', 'address'], + tables_audit: false, + default_showing_table: 'customer', + }; + + const mockConnectionSettingsResponse = { + id: '98a20557-6b38-46aa-b09b-d8a716421dd6', + connectionId: '63f804e4-8588-4957-8d7f-655e2309fef7', + ...mockConnectionSettings, + }; + + beforeEach(async () => { + const matSnackBarSpy = { open: vi.fn() }; + + await TestBed.configureTestingModule({ + imports: [ + MatSnackBarModule, + MatDialogModule, + FormsModule, + MatSelectModule, + BrowserAnimationsModule, + Angulartics2Module.forRoot(), + ConnectionSettingsComponent, + ], + providers: [ + provideHttpClient(), + provideRouter([]), + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ConnectionSettingsComponent), + multi: true, + }, + { provide: MatSnackBar, useValue: matSnackBarSpy }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConnectionSettingsComponent); + component = fixture.componentInstance; + tablesService = TestBed.inject(TablesService); + connectionsService = TestBed.inject(ConnectionsService); + vi.spyOn(connectionsService, 'currentConnectionID', 'get').mockReturnValue('12345678'); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set table list', () => { + const fakeFetchTables = vi.spyOn(tablesService, 'fetchTables').mockReturnValue(of(mockTablesList)); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(fakeFetchTables).toHaveBeenCalledWith('12345678', true); + expect(component.tablesList).toEqual([ + { + table: 'customer', + permissions: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + normalizedTableName: 'Customers', + }, + { + table: 'Orders', + permissions: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + display_name: 'Created orders', + }, + { + table: 'product', + permissions: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + normalizedTableName: 'Products', + }, + ]); + }); + + it('should show error if db is empty', () => { + const fakeFetchTables = vi.spyOn(tablesService, 'fetchTables').mockReturnValue(of([])); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(fakeFetchTables).toHaveBeenCalledWith('12345678', true); + expect(component.noTablesError).toBe(true); + }); + + it('should set table settings if they are existed', () => { + const fakeGetSettings = vi + .spyOn(connectionsService, 'getConnectionSettings') + .mockReturnValue(of(mockConnectionSettingsResponse)); + + component.getSettings(); + + expect(fakeGetSettings).toHaveBeenCalledWith('12345678'); + expect(component.connectionSettings).toEqual(mockConnectionSettingsResponse); + expect(component.isSettingsExist).toBe(true); + }); + + it('should set empty settings if they are not existed', () => { + const fakeGetSettings = vi.spyOn(connectionsService, 'getConnectionSettings').mockReturnValue(of(null)); + + component.getSettings(); + + expect(fakeGetSettings).toHaveBeenCalledWith('12345678'); + expect(component.connectionSettings).toEqual({ + hidden_tables: [], + default_showing_table: null, + primary_color: '', + secondary_color: '', + logo_url: '', + company_name: '', + tables_audit: true, + }); + expect(component.isSettingsExist).toBe(false); + }); + + it('should create settings', () => { + component.connectionSettings = mockConnectionSettings; + const fakeCreateSettings = vi.spyOn(connectionsService, 'createConnectionSettings').mockReturnValue(of()); + + component.createSettings(); + + expect(fakeCreateSettings).toHaveBeenCalledWith('12345678', { + primary_color: '#1F5CB8', + secondary_color: '#F9D648', + logo_url: + 'https://www.shutterstock.com/image-vector/abstract-yellow-grunge-texture-isolated-260nw-1981157192.jpg', + company_name: 'Such.Ukr.Lit', + hidden_tables: ['writer_info', 'address'], + default_showing_table: 'customer', + tables_audit: false, + }); + expect(component.submitting).toBe(false); + }); + + it('should update settings', () => { + component.connectionSettings = mockConnectionSettings; + const fakeUpdateSettings = vi.spyOn(connectionsService, 'updateConnectionSettings').mockReturnValue(of()); + + component.updateSettings(); + + expect(fakeUpdateSettings).toHaveBeenCalledWith('12345678', { + primary_color: '#1F5CB8', + secondary_color: '#F9D648', + logo_url: + 'https://www.shutterstock.com/image-vector/abstract-yellow-grunge-texture-isolated-260nw-1981157192.jpg', + company_name: 'Such.Ukr.Lit', + hidden_tables: ['writer_info', 'address'], + default_showing_table: 'customer', + tables_audit: false, + }); + expect(component.submitting).toBe(false); + }); + + it('should reset settings', () => { + const fakeDeleteSettings = vi.spyOn(connectionsService, 'deleteConnectionSettings').mockReturnValue(of()); + + component.resetSettings(); + + expect(fakeDeleteSettings).toHaveBeenCalledWith('12345678'); + expect(component.submitting).toBe(false); + }); }); diff --git a/frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.spec.ts index 0556bdcd5..7e026e6f4 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-bulk-action-confirmation-dialog/db-bulk-action-confirmation-dialog.component.spec.ts @@ -1,73 +1,79 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; - -import { BbBulkActionConfirmationDialogComponent } from './db-bulk-action-confirmation-dialog.component'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { TablesService } from 'src/app/services/tables.service'; -import { of } from 'rxjs'; -import { Angulartics2Module } from 'angulartics2'; -import { provideHttpClient } from '@angular/common/http'; import { provideRouter } from '@angular/router'; +import { Angulartics2Module } from 'angulartics2'; +import { of } from 'rxjs'; +import { TablesService } from 'src/app/services/tables.service'; +import { BbBulkActionConfirmationDialogComponent } from './db-bulk-action-confirmation-dialog.component'; describe('BbBulkActionConfirmationDialogComponent', () => { - let component: BbBulkActionConfirmationDialogComponent; - let fixture: ComponentFixture; - let tablesService: TablesService; + let component: BbBulkActionConfirmationDialogComponent; + let fixture: ComponentFixture; + let tablesService: TablesService; - const mockDialogRef = { - close: () => { } - }; + const mockDialogRef = { + close: () => {}, + }; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - MatDialogModule, - Angulartics2Module.forRoot(), - BbBulkActionConfirmationDialogComponent - ], - providers: [ - provideHttpClient(), - provideRouter([]), - { provide: MAT_DIALOG_DATA, useValue: { - primaryKeys: [{ - column_name: 'id' - }], - selectedRows: [{ - 'id': '1234', - 'name': 'Anna' - }, - { - 'id': '5678', - 'name': 'John' - }] - }}, - { provide: MatDialogRef, useValue: mockDialogRef }, - ], - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MatSnackBarModule, + MatDialogModule, + Angulartics2Module.forRoot(), + BbBulkActionConfirmationDialogComponent, + ], + providers: [ + provideHttpClient(), + provideRouter([]), + { + provide: MAT_DIALOG_DATA, + useValue: { + primaryKeys: [ + { + column_name: 'id', + }, + ], + selectedRows: [ + { + id: '1234', + name: 'Anna', + }, + { + id: '5678', + name: 'John', + }, + ], + }, + }, + { provide: MatDialogRef, useValue: mockDialogRef }, + ], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(BbBulkActionConfirmationDialogComponent); - component = fixture.componentInstance; - tablesService = TestBed.inject(TablesService); - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(BbBulkActionConfirmationDialogComponent); + component = fixture.componentInstance; + tablesService = TestBed.inject(TablesService); + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should call delete rows if no action id', () => { - component.connectionID = '12345678'; - component.selectedTableName = 'users'; - component.data.title = 'delete rows'; - component.data.primaryKeys = [{id: 1}, {id: 2}, {id: 3}]; - const fakeDeleteRows = vi.spyOn(tablesService, 'bulkDelete').mockReturnValue(of()); + it('should call delete rows if no action id', () => { + component.connectionID = '12345678'; + component.selectedTableName = 'users'; + component.data.title = 'delete rows'; + component.data.primaryKeys = [{ id: 1 }, { id: 2 }, { id: 3 }]; + const fakeDeleteRows = vi.spyOn(tablesService, 'bulkDelete').mockReturnValue(of()); - component.handleConfirmedActions(); + component.handleConfirmedActions(); - expect(fakeDeleteRows).toHaveBeenCalledWith('12345678', 'users', [{id: 1}, {id: 2}, {id: 3}]); - expect(component.submitting).toBe(false); - }); + expect(fakeDeleteRows).toHaveBeenCalledWith('12345678', 'users', [{ id: 1 }, { id: 2 }, { id: 3 }]); + expect(component.submitting).toBe(false); + }); }); diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.spec.ts index 14b2d93cc..dd34b9b79 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-settings/db-table-settings.component.spec.ts @@ -1,179 +1,172 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; - -import { DbTableSettingsComponent } from './db-table-settings.component'; -import { MatDialogModule } from '@angular/material/dialog'; import { FormsModule, NgForm } from '@angular/forms'; -import { MatSelectModule } from '@angular/material/select'; -import { MatRadioModule } from '@angular/material/radio'; +import { MatDialogModule } from '@angular/material/dialog'; import { MatInputModule } from '@angular/material/input'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { TablesService } from 'src/app/services/tables.service'; +import { provideRouter } from '@angular/router'; +import { Angulartics2Module } from 'angulartics2'; import { of } from 'rxjs'; -import { TableSettings, TableOrdering } from 'src/app/models/table'; +import { TableOrdering, TableSettings } from 'src/app/models/table'; import { ConnectionsService } from 'src/app/services/connections.service'; -import { Angulartics2Module } from 'angulartics2'; -import { provideHttpClient } from '@angular/common/http'; -import { provideRouter } from '@angular/router'; +import { TablesService } from 'src/app/services/tables.service'; +import { DbTableSettingsComponent } from './db-table-settings.component'; describe('DbTableSettingsComponent', () => { - let component: DbTableSettingsComponent; - let fixture: ComponentFixture; - let tablesService: TablesService; - let connectionsService: ConnectionsService; - - const fakeFirstName = { - "column_name": "FirstName", - "column_default": null, - "data_type": "varchar", - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": false, - "character_maximum_length": 30 - }; - const fakeId = { - "column_name": "Id", - "column_default": "auto_increment", - "data_type": "int", - "isExcluded": false, - "isSearched": false, - "auto_increment": true, - "allow_null": false, - "character_maximum_length": null - }; - const fakeBool = { - "column_name": "bool", - "column_default": null, - "data_type": "tinyint", - "isExcluded": false, - "isSearched": true, - "auto_increment": false, - "allow_null": true, - "character_maximum_length": 1 - }; - - const mockTableStructure = { - "structure": [ - fakeFirstName, - fakeId, - fakeBool - ], - "primaryColumns": [ - { - "data_type": "int", - "column_name": "Id" - } - ], - "foreignKeys": [ - { - "referenced_column_name": "CustomerId", - "referenced_table_name": "Customers", - "constraint_name": "Orders_ibfk_2", - "column_name": "Id" - } - ], - "readonly_fields": [], - "table_widgets": [] - } - - const mockTableSettings: TableSettings = { - // id: "f3df6ca8-18af-4347-9777-47c086d83969", - table_name: "actor", - display_name: "", - icon: "", - search_fields: [], - excluded_fields: [], - list_fields: [], - // identification_fields: [], - // list_per_page: null, - ordering: TableOrdering.Ascending, - ordering_field: "", - identity_column: "", - readonly_fields: [], - sortable_by: [], - autocomplete_columns: [ - "FirstName" - ], - columns_view: [], - sensitive_fields: [], - connection_id: "63f804e4-8588-4957-8d7f-655e2309fef7", - allow_csv_export: true, - allow_csv_import: true, - can_delete: true, - } - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - MatDialogModule, - FormsModule, - MatSelectModule, - MatRadioModule, - MatInputModule, - BrowserAnimationsModule, - Angulartics2Module.forRoot(), - DbTableSettingsComponent - ], - providers: [provideHttpClient(), provideRouter([])] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DbTableSettingsComponent); - component = fixture.componentInstance; - tablesService = TestBed.inject(TablesService); - connectionsService = TestBed.inject(ConnectionsService); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should set initial state', () => { - vi.spyOn(connectionsService, 'currentConnectionID', 'get').mockReturnValue('12345678'); - vi.spyOn(tablesService, 'currentTableName', 'get').mockReturnValue('users'); - vi.spyOn(tablesService, 'fetchTableStructure').mockReturnValue(of(mockTableStructure)); - vi.spyOn(tablesService, 'fetchTableSettings').mockReturnValue(of(mockTableSettings)); - - component.ngOnInit(); - fixture.detectChanges(); - - expect(component.fields).toEqual(['FirstName', 'Id', 'bool']); - expect(component.fields_to_exclude).toEqual(['FirstName', 'bool']); - expect(component.tableSettings).toEqual(mockTableSettings); - }); - - it('should update settings', () => { - const fakeUpdateTableSettings = vi.spyOn(tablesService, 'updateTableSettings').mockReturnValue(of()); - component.isSettingsExist = true; - component.connectionID = '12345678'; - component.tableName = 'users'; - component.tableSettings = mockTableSettings; - - component.updateSettings(); - - expect(fakeUpdateTableSettings).toHaveBeenCalledWith(true, '12345678', 'users', mockTableSettings); - }); - - it('should delete settings', () => { - // const fakeUpdateTableSettings = vi.spyOn(tablesService, 'updateTableSettings').mockReturnValue(of()); - // component.isSettingsExist = true; - component.connectionID = '12345678'; - component.tableName = 'users'; - // component.tableSettings = mockTableSettings; - const fakeDeleteSettings = vi.spyOn(tablesService, 'deleteTableSettings').mockReturnValue(of()); - - const testForm = { - value: { - name: "tableSettingsForm", - } - }; - - component.resetSettings(testForm); - - expect(fakeDeleteSettings).toHaveBeenCalledWith('12345678', 'users'); - }); + let component: DbTableSettingsComponent; + let fixture: ComponentFixture; + let tablesService: TablesService; + let connectionsService: ConnectionsService; + + const fakeFirstName = { + column_name: 'FirstName', + column_default: null, + data_type: 'varchar', + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: false, + character_maximum_length: 30, + }; + const fakeId = { + column_name: 'Id', + column_default: 'auto_increment', + data_type: 'int', + isExcluded: false, + isSearched: false, + auto_increment: true, + allow_null: false, + character_maximum_length: null, + }; + const fakeBool = { + column_name: 'bool', + column_default: null, + data_type: 'tinyint', + isExcluded: false, + isSearched: true, + auto_increment: false, + allow_null: true, + character_maximum_length: 1, + }; + + const mockTableStructure = { + structure: [fakeFirstName, fakeId, fakeBool], + primaryColumns: [ + { + data_type: 'int', + column_name: 'Id', + }, + ], + foreignKeys: [ + { + referenced_column_name: 'CustomerId', + referenced_table_name: 'Customers', + constraint_name: 'Orders_ibfk_2', + column_name: 'Id', + }, + ], + readonly_fields: [], + table_widgets: [], + }; + + const mockTableSettings: TableSettings = { + // id: "f3df6ca8-18af-4347-9777-47c086d83969", + table_name: 'actor', + display_name: '', + icon: '', + search_fields: [], + excluded_fields: [], + list_fields: [], + // identification_fields: [], + // list_per_page: null, + ordering: TableOrdering.Ascending, + ordering_field: '', + identity_column: '', + readonly_fields: [], + sortable_by: [], + autocomplete_columns: ['FirstName'], + columns_view: [], + sensitive_fields: [], + connection_id: '63f804e4-8588-4957-8d7f-655e2309fef7', + allow_csv_export: true, + allow_csv_import: true, + can_delete: true, + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MatSnackBarModule, + MatDialogModule, + FormsModule, + MatSelectModule, + MatRadioModule, + MatInputModule, + BrowserAnimationsModule, + Angulartics2Module.forRoot(), + DbTableSettingsComponent, + ], + providers: [provideHttpClient(), provideRouter([])], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DbTableSettingsComponent); + component = fixture.componentInstance; + tablesService = TestBed.inject(TablesService); + connectionsService = TestBed.inject(ConnectionsService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set initial state', () => { + vi.spyOn(connectionsService, 'currentConnectionID', 'get').mockReturnValue('12345678'); + vi.spyOn(tablesService, 'currentTableName', 'get').mockReturnValue('users'); + vi.spyOn(tablesService, 'fetchTableStructure').mockReturnValue(of(mockTableStructure)); + vi.spyOn(tablesService, 'fetchTableSettings').mockReturnValue(of(mockTableSettings)); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.fields).toEqual(['FirstName', 'Id', 'bool']); + expect(component.fields_to_exclude).toEqual(['FirstName', 'bool']); + expect(component.tableSettings).toEqual(mockTableSettings); + }); + + it('should update settings', () => { + const fakeUpdateTableSettings = vi.spyOn(tablesService, 'updateTableSettings').mockReturnValue(of()); + component.isSettingsExist = true; + component.connectionID = '12345678'; + component.tableName = 'users'; + component.tableSettings = mockTableSettings; + + component.updateSettings(); + + expect(fakeUpdateTableSettings).toHaveBeenCalledWith(true, '12345678', 'users', mockTableSettings); + }); + + it('should delete settings', () => { + // const fakeUpdateTableSettings = vi.spyOn(tablesService, 'updateTableSettings').mockReturnValue(of()); + // component.isSettingsExist = true; + component.connectionID = '12345678'; + component.tableName = 'users'; + // component.tableSettings = mockTableSettings; + const fakeDeleteSettings = vi.spyOn(tablesService, 'deleteTableSettings').mockReturnValue(of()); + + const testForm = { + value: { + name: 'tableSettingsForm', + }, + }; + + component.resetSettings(testForm); + + expect(fakeDeleteSettings).toHaveBeenCalledWith('12345678', 'users'); + }); }); diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts index ce2c02e36..142585512 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-dialog/saved-filters-dialog.component.spec.ts @@ -1,254 +1,251 @@ -import { ActivatedRoute, } from '@angular/router'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; - -import { ConnectionsService } from 'src/app/services/connections.service'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { SavedFiltersDialogComponent } from './saved-filters-dialog.component'; -import { TablesService } from 'src/app/services/tables.service'; -import { of } from 'rxjs'; import { Angulartics2Module } from 'angulartics2'; +import { of } from 'rxjs'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { TablesService } from 'src/app/services/tables.service'; +import { SavedFiltersDialogComponent } from './saved-filters-dialog.component'; describe('SavedFiltersDialogComponent', () => { - let component: SavedFiltersDialogComponent; - let fixture: ComponentFixture; - let tablesServiceMock: any; - let _connectionsServiceMock: any; - - beforeEach(async () => { - const tableSpy = { - cast: { subscribe: vi.fn() }, - createSavedFilter: vi.fn(), - deleteSavedFilter: vi.fn(), - updateSavedFilter: vi.fn() - }; - - const connectionSpy = { - currentConnection: { type: 'postgres' } - }; - - await TestBed.configureTestingModule({ - imports: [ - SavedFiltersDialogComponent, - RouterTestingModule, - Angulartics2Module.forRoot(), - ], - providers: [ - { provide: TablesService, useValue: tableSpy }, - { provide: ConnectionsService, useValue: connectionSpy }, - { provide: MatDialogRef, useValue: { close: vi.fn() } }, - { provide: MatSnackBar, useValue: { open: vi.fn() } }, - { - provide: ActivatedRoute, - useValue: { - snapshot: { - paramMap: { get: () => {} }, - queryParamMap: { get: () => {} } - } - } - }, - { provide: MAT_DIALOG_DATA, useValue: { - connectionID: '123', - tableName: 'test_table', - displayTableName: 'Test Table', - filtersSet: { - name: 'Test Filter', - filters: {} - }, - structure: [], - tableForeignKeys: {}, - tableWidgets: [] - }} - ] - }) - .compileComponents(); - - tablesServiceMock = TestBed.inject(TablesService); - _connectionsServiceMock = TestBed.inject(ConnectionsService); - - fixture = TestBed.createComponent(SavedFiltersDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should toggle dynamic column', () => { - const fieldName = 'test_field'; - - // Initially null - expect(component.dynamicColumn).toBeNull(); - - // First toggle sets to fieldName - component.toggleDynamicColumn(fieldName); - expect(component.dynamicColumn).toBe(fieldName); - - // Second toggle sets back to null - component.toggleDynamicColumn(fieldName); - expect(component.dynamicColumn).toBeNull(); - }); - - it('should exclude dynamic column from filters and include it with column_name and comparator', () => { - // Setup - const fieldName = 'test_field'; - component.tableRowFieldsShown = { [fieldName]: 'test_value' }; - component.tableRowFieldsComparator = { [fieldName]: 'eq' }; - component.dynamicColumn = fieldName; - tablesServiceMock.createSavedFilter.mockReturnValue(of({})); - - // Call handleSaveFilters - component.handleSaveFilters(); - - // Verify - filters should be empty, and dynamic_column should have column_name and comparator - expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( - component.data.connectionID, - component.data.tableName, - { - name: component.data.filtersSet.name, - filters: { }, - dynamic_column: { - column_name: fieldName, - comparator: 'eq' - } - } - ); - }); - - it('should include dynamic column with column_name and comparator and exclude it from filters', () => { - // Setup - const fieldName = 'test_field'; - const dynamicFieldName = 'dynamic_field'; - component.tableRowFieldsShown = { - [fieldName]: 'test_value', - [dynamicFieldName]: 'dynamic_value' - }; - component.tableRowFieldsComparator = { - [fieldName]: 'eq', - [dynamicFieldName]: 'contains' - }; - component.dynamicColumn = dynamicFieldName; // Different field from the one with comparator - tablesServiceMock.createSavedFilter.mockReturnValue(of({})); - - // Call handleSaveFilters - component.handleSaveFilters(); - - // Verify - only fieldName in filters, dynamicFieldName in dynamic_column - expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( - component.data.connectionID, - component.data.tableName, - { - name: component.data.filtersSet.name, - filters: { [fieldName]: { eq: 'test_value' } }, - dynamic_column: { - column_name: dynamicFieldName, - comparator: 'contains' - } - } - ); - }); - - it('should handle empty values in filters as null', () => { - // Setup - const fieldName = 'test_field'; - component.tableRowFieldsShown = { [fieldName]: '' }; // Empty value - component.tableRowFieldsComparator = { [fieldName]: 'eq' }; - tablesServiceMock.createSavedFilter.mockReturnValue(of({})); - - // Call handleSaveFilters - component.handleSaveFilters(); - - // Verify - value should be null instead of empty string - expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( - component.data.connectionID, - component.data.tableName, - { - name: component.data.filtersSet.name, - filters: { [fieldName]: { eq: null } } - } - ); - }); - - it('should handle dynamic column with no comparator', () => { - // Setup - const fieldName = 'test_field'; - const dynamicFieldName = 'dynamic_field_no_comparator'; - component.tableRowFieldsShown = { - [fieldName]: 'test_value', - [dynamicFieldName]: 'dynamic_value' - }; - component.tableRowFieldsComparator = { - [fieldName]: 'eq' - // No comparator for dynamicFieldName - }; - component.dynamicColumn = dynamicFieldName; - tablesServiceMock.createSavedFilter.mockReturnValue(of({})); - - // Call handleSaveFilters - component.handleSaveFilters(); - - // Verify - dynamic_column should have empty comparator - expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( - component.data.connectionID, - component.data.tableName, - { - name: component.data.filtersSet.name, - filters: { [fieldName]: { eq: 'test_value' } }, - dynamic_column: { - column_name: dynamicFieldName, - comparator: '' - } - } - ); - }); - - it('should update existing filter when id is present', () => { - // Setup - const fieldName = 'test_field'; - const filterId = '123'; - component.tableRowFieldsShown = { [fieldName]: 'test_value' }; - component.tableRowFieldsComparator = { [fieldName]: 'eq' }; - component.data.filtersSet.id = filterId; - tablesServiceMock.updateSavedFilter.mockReturnValue(of({})); - - // Call handleSaveFilters - component.handleSaveFilters(); - - // Verify updateSavedFilter is called instead of createSavedFilter - expect(tablesServiceMock.updateSavedFilter).toHaveBeenCalledWith( - component.data.connectionID, - component.data.tableName, - filterId, - { - name: component.data.filtersSet.name, - filters: { [fieldName]: { eq: 'test_value' } } - } - ); - expect(tablesServiceMock.createSavedFilter).not.toHaveBeenCalled(); - }); - - it('should create new filter when id is not present', () => { - // Setup - const fieldName = 'test_field'; - component.tableRowFieldsShown = { [fieldName]: 'test_value' }; - component.tableRowFieldsComparator = { [fieldName]: 'eq' }; - component.data.filtersSet.id = undefined; // No ID means creating a new filter - tablesServiceMock.createSavedFilter.mockReturnValue(of({})); - - // Call handleSaveFilters - component.handleSaveFilters(); - - // Verify createSavedFilter is called - expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( - component.data.connectionID, - component.data.tableName, - { - name: component.data.filtersSet.name, - filters: { [fieldName]: { eq: 'test_value' } } - } - ); - expect(tablesServiceMock.updateSavedFilter).not.toHaveBeenCalled(); - }); + let component: SavedFiltersDialogComponent; + let fixture: ComponentFixture; + let tablesServiceMock: any; + let _connectionsServiceMock: any; + + beforeEach(async () => { + const tableSpy = { + cast: { subscribe: vi.fn() }, + createSavedFilter: vi.fn(), + deleteSavedFilter: vi.fn(), + updateSavedFilter: vi.fn(), + }; + + const connectionSpy = { + currentConnection: { type: 'postgres' }, + }; + + await TestBed.configureTestingModule({ + imports: [SavedFiltersDialogComponent, RouterTestingModule, Angulartics2Module.forRoot()], + providers: [ + { provide: TablesService, useValue: tableSpy }, + { provide: ConnectionsService, useValue: connectionSpy }, + { provide: MatDialogRef, useValue: { close: vi.fn() } }, + { provide: MatSnackBar, useValue: { open: vi.fn() } }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: { get: () => {} }, + queryParamMap: { get: () => {} }, + }, + }, + }, + { + provide: MAT_DIALOG_DATA, + useValue: { + connectionID: '123', + tableName: 'test_table', + displayTableName: 'Test Table', + filtersSet: { + name: 'Test Filter', + filters: {}, + }, + structure: [], + tableForeignKeys: {}, + tableWidgets: [], + }, + }, + ], + }).compileComponents(); + + tablesServiceMock = TestBed.inject(TablesService); + _connectionsServiceMock = TestBed.inject(ConnectionsService); + + fixture = TestBed.createComponent(SavedFiltersDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should toggle dynamic column', () => { + const fieldName = 'test_field'; + + // Initially null + expect(component.dynamicColumn).toBeNull(); + + // First toggle sets to fieldName + component.toggleDynamicColumn(fieldName); + expect(component.dynamicColumn).toBe(fieldName); + + // Second toggle sets back to null + component.toggleDynamicColumn(fieldName); + expect(component.dynamicColumn).toBeNull(); + }); + + it('should exclude dynamic column from filters and include it with column_name and comparator', () => { + // Setup + const fieldName = 'test_field'; + component.tableRowFieldsShown = { [fieldName]: 'test_value' }; + component.tableRowFieldsComparator = { [fieldName]: 'eq' }; + component.dynamicColumn = fieldName; + tablesServiceMock.createSavedFilter.mockReturnValue(of({})); + + // Call handleSaveFilters + component.handleSaveFilters(); + + // Verify - filters should be empty, and dynamic_column should have column_name and comparator + expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + { + name: component.data.filtersSet.name, + filters: {}, + dynamic_column: { + column_name: fieldName, + comparator: 'eq', + }, + }, + ); + }); + + it('should include dynamic column with column_name and comparator and exclude it from filters', () => { + // Setup + const fieldName = 'test_field'; + const dynamicFieldName = 'dynamic_field'; + component.tableRowFieldsShown = { + [fieldName]: 'test_value', + [dynamicFieldName]: 'dynamic_value', + }; + component.tableRowFieldsComparator = { + [fieldName]: 'eq', + [dynamicFieldName]: 'contains', + }; + component.dynamicColumn = dynamicFieldName; // Different field from the one with comparator + tablesServiceMock.createSavedFilter.mockReturnValue(of({})); + + // Call handleSaveFilters + component.handleSaveFilters(); + + // Verify - only fieldName in filters, dynamicFieldName in dynamic_column + expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + { + name: component.data.filtersSet.name, + filters: { [fieldName]: { eq: 'test_value' } }, + dynamic_column: { + column_name: dynamicFieldName, + comparator: 'contains', + }, + }, + ); + }); + + it('should handle empty values in filters as null', () => { + // Setup + const fieldName = 'test_field'; + component.tableRowFieldsShown = { [fieldName]: '' }; // Empty value + component.tableRowFieldsComparator = { [fieldName]: 'eq' }; + tablesServiceMock.createSavedFilter.mockReturnValue(of({})); + + // Call handleSaveFilters + component.handleSaveFilters(); + + // Verify - value should be null instead of empty string + expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + { + name: component.data.filtersSet.name, + filters: { [fieldName]: { eq: null } }, + }, + ); + }); + + it('should handle dynamic column with no comparator', () => { + // Setup + const fieldName = 'test_field'; + const dynamicFieldName = 'dynamic_field_no_comparator'; + component.tableRowFieldsShown = { + [fieldName]: 'test_value', + [dynamicFieldName]: 'dynamic_value', + }; + component.tableRowFieldsComparator = { + [fieldName]: 'eq', + // No comparator for dynamicFieldName + }; + component.dynamicColumn = dynamicFieldName; + tablesServiceMock.createSavedFilter.mockReturnValue(of({})); + + // Call handleSaveFilters + component.handleSaveFilters(); + + // Verify - dynamic_column should have empty comparator + expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + { + name: component.data.filtersSet.name, + filters: { [fieldName]: { eq: 'test_value' } }, + dynamic_column: { + column_name: dynamicFieldName, + comparator: '', + }, + }, + ); + }); + + it('should update existing filter when id is present', () => { + // Setup + const fieldName = 'test_field'; + const filterId = '123'; + component.tableRowFieldsShown = { [fieldName]: 'test_value' }; + component.tableRowFieldsComparator = { [fieldName]: 'eq' }; + component.data.filtersSet.id = filterId; + tablesServiceMock.updateSavedFilter.mockReturnValue(of({})); + + // Call handleSaveFilters + component.handleSaveFilters(); + + // Verify updateSavedFilter is called instead of createSavedFilter + expect(tablesServiceMock.updateSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + filterId, + { + name: component.data.filtersSet.name, + filters: { [fieldName]: { eq: 'test_value' } }, + }, + ); + expect(tablesServiceMock.createSavedFilter).not.toHaveBeenCalled(); + }); + + it('should create new filter when id is not present', () => { + // Setup + const fieldName = 'test_field'; + component.tableRowFieldsShown = { [fieldName]: 'test_value' }; + component.tableRowFieldsComparator = { [fieldName]: 'eq' }; + component.data.filtersSet.id = undefined; // No ID means creating a new filter + tablesServiceMock.createSavedFilter.mockReturnValue(of({})); + + // Call handleSaveFilters + component.handleSaveFilters(); + + // Verify createSavedFilter is called + expect(tablesServiceMock.createSavedFilter).toHaveBeenCalledWith( + component.data.connectionID, + component.data.tableName, + { + name: component.data.filtersSet.name, + filters: { [fieldName]: { eq: 'test_value' } }, + }, + ); + expect(tablesServiceMock.updateSavedFilter).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts index 73103dd82..0276d39b4 100644 --- a/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/saved-filters-panel/saved-filters-panel.component.spec.ts @@ -1,193 +1,190 @@ -import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; - +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, convertToParamMap, Router } from '@angular/router'; import { Angulartics2Module } from 'angulartics2'; +import { of } from 'rxjs'; import { ConnectionsService } from 'src/app/services/connections.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { MatDialog } from '@angular/material/dialog'; -import { SavedFiltersPanelComponent } from './saved-filters-panel.component'; import { TablesService } from 'src/app/services/tables.service'; -import { of } from 'rxjs'; +import { SavedFiltersPanelComponent } from './saved-filters-panel.component'; // We need to mock the JsonURL import used in the component class JsonURLMock { - static stringify(obj: any): string { - return JSON.stringify(obj); - } - - static parse(str: string): any { - try { - return JSON.parse(str); - } catch (_e) { - return {}; - } - } + static stringify(obj: any): string { + return JSON.stringify(obj); + } + + static parse(str: string): any { + try { + return JSON.parse(str); + } catch (_e) { + return {}; + } + } } // Add to global scope to be used by the component (window as any).JsonURL = JsonURLMock; describe('SavedFiltersPanelComponent', () => { - let component: SavedFiltersPanelComponent; - let fixture: ComponentFixture; - let _tablesServiceSpy: TablesService; - let _routerSpy: Router; - - const mockFilter = { - id: 'filter1', - name: 'Test Filter', - filters: { - name: { eq: 'John' }, - age: { gt: 25 } - }, - dynamic_column: { - column_name: 'city', - comparator: 'eq' - } - }; - - beforeEach(async () => { - const tablesServiceMock = { - getSavedFilters: vi.fn().mockReturnValue(of([mockFilter])), - createSavedFilter: vi.fn(), - cast: of({}), - }; - - const routerMock = { - navigate: vi.fn(), - }; - - const activatedRouteMock = { - queryParams: of({}), - paramMap: of(convertToParamMap({})), - queryParamMap: of(convertToParamMap({})), - snapshot: { - queryParams: {}, - paramMap: { - get: (_key: string) => null - }, - queryParamMap: { - get: (_key: string) => null - } - } - }; - - const matDialogMock = { - open: vi.fn(), - }; - - const connectionsServiceMock = { - get currentConnection() { return { type: 'postgres' }; } - }; - - await TestBed.configureTestingModule({ - imports: [ - SavedFiltersPanelComponent, - HttpClientTestingModule, - Angulartics2Module.forRoot(), - ], - providers: [ - { provide: TablesService, useValue: tablesServiceMock }, - { provide: ConnectionsService, useValue: connectionsServiceMock }, - { provide: Router, useValue: routerMock }, - { provide: ActivatedRoute, useValue: activatedRouteMock }, - { provide: MatDialog, useValue: matDialogMock } - ] - }).compileComponents(); - - _tablesServiceSpy = TestBed.inject(TablesService); - _routerSpy = TestBed.inject(Router); - - fixture = TestBed.createComponent(SavedFiltersPanelComponent); - component = fixture.componentInstance; - component.connectionID = 'conn1'; - component.selectedTableName = 'users'; - component.structure = []; - component.tableTypes = {}; - component.selectedTableDisplayName = 'Users'; - component.tableForeignKeys = []; - - // Mock filterSelected event emitter - vi.spyOn(component.filterSelected, 'emit'); - - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should process filters data to separate static filters and dynamic column', () => { - const result = component.processFiltersData(mockFilter); - - expect(result.dynamicColumn).toBeTruthy(); - expect(result.dynamicColumn?.column).toBe('city'); - expect(result.dynamicColumn?.operator).toBe('eq'); - - expect(result.staticFilters.length).toBe(2); - expect(result.staticFilters[0].column).toBe('name'); - expect(result.staticFilters[1].column).toBe('age'); - }); - - it('should update dynamic column comparator', () => { - component.selectedFilterSetId = 'filter1'; - component.savedFilterMap = { - filter1: { - dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' } - } - }; - - component.updateDynamicColumnComparator('contains'); - - expect(component.savedFilterMap.filter1.dynamicColumn.operator).toBe('contains'); - }); - - it('should set value to empty string when comparator is empty', () => { - // Setup component with the minimal required properties - component.selectedFilterSetId = 'filter1'; - component.savedFilterMap = { - filter1: { - dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' }, - filters: { city: { eq: 'New York' } } - } - }; - - // Spy on applyDynamicColumnChanges to prevent it from executing - vi.spyOn(component, 'applyDynamicColumnChanges'); - - // Call the method under test - component.updateDynamicColumnComparator('empty'); - - // Verify the value was set to empty string - expect(component.savedFilterMap.filter1.dynamicColumn.value).toBe(''); - }); - - it('should update dynamic column value', () => { - // Setup component with the minimal required properties - component.selectedFilterSetId = 'filter1'; - component.savedFilterMap = { - filter1: { - dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' }, - filters: { city: { eq: 'New York' } } - } - }; - - // Spy on applyDynamicColumnChanges to prevent it from executing - vi.spyOn(component, 'applyDynamicColumnChanges'); - - // Replace setTimeout with a function that executes immediately - vi.spyOn(window, 'setTimeout').mockImplementation((fn: TimerHandler) => { - // Execute function immediately instead of waiting - if (typeof fn === 'function') fn(); - // Return a fake timer ID - return 999 as unknown as ReturnType; - }); - - // Call the method under test - component.updateDynamicColumnValue('Chicago'); - - // Verify the value was updated - expect(component.savedFilterMap.filter1.dynamicColumn.value).toBe('Chicago'); - expect(component.applyDynamicColumnChanges).toHaveBeenCalled(); - }); + let component: SavedFiltersPanelComponent; + let fixture: ComponentFixture; + let _tablesServiceSpy: TablesService; + let _routerSpy: Router; + + const mockFilter = { + id: 'filter1', + name: 'Test Filter', + filters: { + name: { eq: 'John' }, + age: { gt: 25 }, + }, + dynamic_column: { + column_name: 'city', + comparator: 'eq', + }, + }; + + beforeEach(async () => { + const tablesServiceMock = { + getSavedFilters: vi.fn().mockReturnValue(of([mockFilter])), + createSavedFilter: vi.fn(), + cast: of({}), + }; + + const routerMock = { + navigate: vi.fn(), + }; + + const activatedRouteMock = { + queryParams: of({}), + paramMap: of(convertToParamMap({})), + queryParamMap: of(convertToParamMap({})), + snapshot: { + queryParams: {}, + paramMap: { + get: (_key: string) => null, + }, + queryParamMap: { + get: (_key: string) => null, + }, + }, + }; + + const matDialogMock = { + open: vi.fn(), + }; + + const connectionsServiceMock = { + get currentConnection() { + return { type: 'postgres' }; + }, + }; + + await TestBed.configureTestingModule({ + imports: [SavedFiltersPanelComponent, HttpClientTestingModule, Angulartics2Module.forRoot()], + providers: [ + { provide: TablesService, useValue: tablesServiceMock }, + { provide: ConnectionsService, useValue: connectionsServiceMock }, + { provide: Router, useValue: routerMock }, + { provide: ActivatedRoute, useValue: activatedRouteMock }, + { provide: MatDialog, useValue: matDialogMock }, + ], + }).compileComponents(); + + _tablesServiceSpy = TestBed.inject(TablesService); + _routerSpy = TestBed.inject(Router); + + fixture = TestBed.createComponent(SavedFiltersPanelComponent); + component = fixture.componentInstance; + component.connectionID = 'conn1'; + component.selectedTableName = 'users'; + component.structure = []; + component.tableTypes = {}; + component.selectedTableDisplayName = 'Users'; + component.tableForeignKeys = []; + + // Mock filterSelected event emitter + vi.spyOn(component.filterSelected, 'emit'); + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should process filters data to separate static filters and dynamic column', () => { + const result = component.processFiltersData(mockFilter); + + expect(result.dynamicColumn).toBeTruthy(); + expect(result.dynamicColumn?.column).toBe('city'); + expect(result.dynamicColumn?.operator).toBe('eq'); + + expect(result.staticFilters.length).toBe(2); + expect(result.staticFilters[0].column).toBe('name'); + expect(result.staticFilters[1].column).toBe('age'); + }); + + it('should update dynamic column comparator', () => { + component.selectedFilterSetId = 'filter1'; + component.savedFilterMap = { + filter1: { + dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' }, + }, + }; + + component.updateDynamicColumnComparator('contains'); + + expect(component.savedFilterMap.filter1.dynamicColumn.operator).toBe('contains'); + }); + + it('should set value to empty string when comparator is empty', () => { + // Setup component with the minimal required properties + component.selectedFilterSetId = 'filter1'; + component.savedFilterMap = { + filter1: { + dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' }, + filters: { city: { eq: 'New York' } }, + }, + }; + + // Spy on applyDynamicColumnChanges to prevent it from executing + vi.spyOn(component, 'applyDynamicColumnChanges'); + + // Call the method under test + component.updateDynamicColumnComparator('empty'); + + // Verify the value was set to empty string + expect(component.savedFilterMap.filter1.dynamicColumn.value).toBe(''); + }); + + it('should update dynamic column value', () => { + // Setup component with the minimal required properties + component.selectedFilterSetId = 'filter1'; + component.savedFilterMap = { + filter1: { + dynamicColumn: { column: 'city', operator: 'eq', value: 'New York' }, + filters: { city: { eq: 'New York' } }, + }, + }; + + // Spy on applyDynamicColumnChanges to prevent it from executing + vi.spyOn(component, 'applyDynamicColumnChanges'); + + // Replace setTimeout with a function that executes immediately + vi.spyOn(window, 'setTimeout').mockImplementation((fn: TimerHandler) => { + // Execute function immediately instead of waiting + if (typeof fn === 'function') fn(); + // Return a fake timer ID + return 999 as unknown as ReturnType; + }); + + // Call the method under test + component.updateDynamicColumnValue('Chicago'); + + // Verify the value was updated + expect(component.savedFilterMap.filter1.dynamicColumn.value).toBe('Chicago'); + expect(component.applyDynamicColumnChanges).toHaveBeenCalled(); + }); }); diff --git a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.spec.ts b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.spec.ts index fed216703..cf5369765 100644 --- a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.spec.ts +++ b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.spec.ts @@ -1,362 +1,345 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; - -import { DbTableRowEditComponent } from './db-table-row-edit.component'; import { MatDialogModule } from '@angular/material/dialog'; -import { TablesService } from 'src/app/services/tables.service'; -import { ConnectionsService } from 'src/app/services/connections.service'; -import { DBtype, Connection, ConnectionType } from 'src/app/models/connection'; -import { Angulartics2Module } from 'angulartics2'; -import { provideHttpClient } from '@angular/common/http'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { provideRouter } from '@angular/router'; +import { Angulartics2Module } from 'angulartics2'; +import { Connection, ConnectionType, DBtype } from 'src/app/models/connection'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { TablesService } from 'src/app/services/tables.service'; +import { DbTableRowEditComponent } from './db-table-row-edit.component'; describe('DbTableRowEditComponent', () => { - let component: DbTableRowEditComponent; - let fixture: ComponentFixture; - let _tablesService: TablesService; - let connectionsService: ConnectionsService; - - beforeEach(async () => { - const matSnackBarSpy = { open: vi.fn() }; - - await TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - MatDialogModule, - Angulartics2Module.forRoot(), - DbTableRowEditComponent - ], - providers:[ - provideHttpClient(), - provideRouter([]), - { provide: MatSnackBar, useValue: matSnackBarSpy } - ] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DbTableRowEditComponent); - component = fixture.componentInstance; - _tablesService = TestBed.inject(TablesService); - connectionsService = TestBed.inject(ConnectionsService); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should set connection id', () => { - vi.spyOn(connectionsService, 'currentConnectionID', 'get').mockReturnValue('12345678'); - - component.ngOnInit(); - fixture.detectChanges(); - - expect(component.connectionID).toEqual('12345678'); - }); - - it('should set structure — define: relation between column name and type, required columns', async() => { - component.tableForeignKeys = [ - { - "referenced_column_name": "Id", - "referenced_table_name": "Products", - "constraint_name": "Orders_ibfk_1", - "column_name": "ProductId" - }, - { - "referenced_column_name": "Id", - "referenced_table_name": "Customers", - "constraint_name": "Orders_ibfk_2", - "column_name": "CustomerId" - } - ]; - - const fakeProduct_categories = { - "column_name": "product_categories", - "column_default": null, - "data_type": "enum", - "data_type_params": [ - "food", - "drinks", - "cleaning" - ], - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": true, - "character_maximum_length": 1 - } - - const fakeCustomer_categories = { - "column_name": "customer_categories", - "column_default": null, - "data_type": "enum", - "data_type_params": [ - "manager", - "seller" - ], - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": true, - "character_maximum_length": 1 - } - - const fakeCustomerId = { - "column_name": "CustomerId", - "column_default": null, - "data_type": "int", - "isExcluded": false, - "isSearched": true, - "auto_increment": false, - "allow_null": false, - "character_maximum_length": null - } - - const fakeProductId = { - "column_name": "ProductId", - "column_default": null, - "data_type": "int", - "isExcluded": false, - "isSearched": true, - "auto_increment": false, - "allow_null": false, - "character_maximum_length": null - } - - const fakeBool = { - "column_name": "bool", - "column_default": null, - "data_type": "tinyint", - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": true, - "character_maximum_length": 1 - } - - const fakeFloat = { - "column_name": "float", - "column_default": null, - "data_type": "float", - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": true, - "character_maximum_length": 102 - } - - const fakeStructure = [ - fakeProduct_categories, - fakeCustomer_categories, - fakeCustomerId, - fakeProductId, - fakeBool, - fakeFloat, - ] - - component.setRowStructure(fakeStructure); - - expect(component.tableRowRequiredValues).toEqual({ - product_categories: false, - customer_categories: false, - CustomerId: true, - ProductId: true, - bool: false, - float: false - }); - expect(component.tableRowStructure).toEqual({ - product_categories: fakeProduct_categories, - customer_categories: fakeCustomer_categories, - CustomerId: fakeCustomerId, - ProductId: fakeProductId, - bool: fakeBool, - float: fakeFloat - }) - }) - - it('should set widgets', () => { - const fakeWidgets = [ - { - "id": "36141f10-feb6-4c42-acdb-261523729625", - "field_name": "CustomerId", - "widget_type": "Textarea", - "widget_params": '', - "name": "Customer", - "description": "" - }, - { - "id": "d6a4caa5-68f6-455f-90ff-2ad81856253b", - "field_name": "Price", - "widget_type": "Number", - "widget_params": '', - "name": "", - "description": "Prices are pointed in USD" - } - ] - - component.setWidgets(fakeWidgets); - - expect(component.tableWidgetsList).toEqual(['CustomerId', 'Price']); - expect(component.tableWidgets).toEqual({ - CustomerId: { - id: "36141f10-feb6-4c42-acdb-261523729625", - field_name: "CustomerId", - widget_type: "Textarea", - widget_params: null, - name: "Customer", - description: "" - }, - Price: { - "id": "d6a4caa5-68f6-455f-90ff-2ad81856253b", - "field_name": "Price", - "widget_type": "Number", - "widget_params": null, - "name": "", - "description": "Prices are pointed in USD" - } - }); - }); - - it('should return foreign key relations by column name', () => { - component.tableForeignKeys = [ - { - "referenced_column_name": "Id", - "referenced_table_name": "Products", - "constraint_name": "Orders_ibfk_1", - "column_name": "ProductId" - }, - { - "referenced_column_name": "Id", - "referenced_table_name": "Customers", - "constraint_name": "Orders_ibfk_2", - "column_name": "CustomerId" - } - ]; - - const foreignKeyRelations = component.getRelations('ProductId'); - expect(foreignKeyRelations).toEqual({ - "referenced_column_name": "Id", - "referenced_table_name": "Products", - "constraint_name": "Orders_ibfk_1", - "column_name": "ProductId" - }) - }); - - it('should check if field is readonly', () => { - component.readonlyFields = ['Id', 'Price']; - - const isPriceReafonly = component.isReadonlyField('Price'); - expect(isPriceReafonly).toBe(true); - }); - - it('should check if field is widget', () => { - component.tableWidgetsList = ['CustomerId', 'Price']; - - const isPriceWidget = component.isWidget('Price'); - expect(isPriceWidget).toBe(true); - }); - - describe('updateField for password widget behavior', () => { - beforeEach(() => { - component.tableRowValues = { - id: 1, - username: 'testuser', - password: '***' - }; - }); - - it('should update tableRowValues when password field receives a value', () => { - component.updateField('newPassword', 'password'); - expect(component.tableRowValues.password).toBe('newPassword'); - }); - - it('should update tableRowValues when password field receives empty string', () => { - component.updateField('', 'password'); - expect(component.tableRowValues.password).toBe(''); - }); - - it('should update tableRowValues when password field receives null (clear password)', () => { - component.updateField(null, 'password'); - expect(component.tableRowValues.password).toBe(null); - }); - - it('should handle password field update alongside other fields', () => { - component.updateField('updatedUser', 'username'); - component.updateField('newPassword', 'password'); - - expect(component.tableRowValues.username).toBe('updatedUser'); - expect(component.tableRowValues.password).toBe('newPassword'); - }); - }); - - describe('getFormattedUpdatedRow', () => { - beforeEach(() => { - vi.spyOn(connectionsService, 'currentConnection', 'get').mockReturnValue({ - id: 'test-id', - database: 'test-db', - title: 'Test Connection', - host: 'localhost', - port: '5432', - sid: null, - type: DBtype.Postgres, - username: 'test-user', - ssh: false, - ssl: false, - cert: '', - masterEncryption: false, - azure_encryption: false, - connectionType: ConnectionType.Direct - } as Connection); - component.tableTypes = {}; - component.nonModifyingFields = []; - component.pageAction = null; - }); - - it('should include password field when it has a value', () => { - component.tableRowValues = { - id: 1, - username: 'testuser', - password: 'newPassword' - }; - - const result = component.getFormattedUpdatedRow(); - expect((result as any).password).toBe('newPassword'); - }); - - it('should include password field when it is null (explicit clear)', () => { - component.tableRowValues = { - id: 1, - username: 'testuser', - password: null - }; - - const result = component.getFormattedUpdatedRow(); - expect((result as any).password).toBe(null); - }); - - it('should include password field when it is empty string', () => { - component.tableRowValues = { - id: 1, - username: 'testuser', - password: '' - }; - - const result = component.getFormattedUpdatedRow(); - expect((result as any).password).toBe(''); - }); - - it('should preserve other fields when password is empty', () => { - component.tableRowValues = { - id: 1, - username: 'testuser', - password: '' - }; - - const result = component.getFormattedUpdatedRow(); - expect((result as any).id).toBe(1); - expect((result as any).username).toBe('testuser'); - expect((result as any).password).toBe(''); - }); - }); + let component: DbTableRowEditComponent; + let fixture: ComponentFixture; + let _tablesService: TablesService; + let connectionsService: ConnectionsService; + + beforeEach(async () => { + const matSnackBarSpy = { open: vi.fn() }; + + await TestBed.configureTestingModule({ + imports: [MatSnackBarModule, MatDialogModule, Angulartics2Module.forRoot(), DbTableRowEditComponent], + providers: [provideHttpClient(), provideRouter([]), { provide: MatSnackBar, useValue: matSnackBarSpy }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DbTableRowEditComponent); + component = fixture.componentInstance; + _tablesService = TestBed.inject(TablesService); + connectionsService = TestBed.inject(ConnectionsService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set connection id', () => { + vi.spyOn(connectionsService, 'currentConnectionID', 'get').mockReturnValue('12345678'); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(component.connectionID).toEqual('12345678'); + }); + + it('should set structure — define: relation between column name and type, required columns', async () => { + component.tableForeignKeys = [ + { + referenced_column_name: 'Id', + referenced_table_name: 'Products', + constraint_name: 'Orders_ibfk_1', + column_name: 'ProductId', + }, + { + referenced_column_name: 'Id', + referenced_table_name: 'Customers', + constraint_name: 'Orders_ibfk_2', + column_name: 'CustomerId', + }, + ]; + + const fakeProduct_categories = { + column_name: 'product_categories', + column_default: null, + data_type: 'enum', + data_type_params: ['food', 'drinks', 'cleaning'], + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: true, + character_maximum_length: 1, + }; + + const fakeCustomer_categories = { + column_name: 'customer_categories', + column_default: null, + data_type: 'enum', + data_type_params: ['manager', 'seller'], + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: true, + character_maximum_length: 1, + }; + + const fakeCustomerId = { + column_name: 'CustomerId', + column_default: null, + data_type: 'int', + isExcluded: false, + isSearched: true, + auto_increment: false, + allow_null: false, + character_maximum_length: null, + }; + + const fakeProductId = { + column_name: 'ProductId', + column_default: null, + data_type: 'int', + isExcluded: false, + isSearched: true, + auto_increment: false, + allow_null: false, + character_maximum_length: null, + }; + + const fakeBool = { + column_name: 'bool', + column_default: null, + data_type: 'tinyint', + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: true, + character_maximum_length: 1, + }; + + const fakeFloat = { + column_name: 'float', + column_default: null, + data_type: 'float', + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: true, + character_maximum_length: 102, + }; + + const fakeStructure = [ + fakeProduct_categories, + fakeCustomer_categories, + fakeCustomerId, + fakeProductId, + fakeBool, + fakeFloat, + ]; + + component.setRowStructure(fakeStructure); + + expect(component.tableRowRequiredValues).toEqual({ + product_categories: false, + customer_categories: false, + CustomerId: true, + ProductId: true, + bool: false, + float: false, + }); + expect(component.tableRowStructure).toEqual({ + product_categories: fakeProduct_categories, + customer_categories: fakeCustomer_categories, + CustomerId: fakeCustomerId, + ProductId: fakeProductId, + bool: fakeBool, + float: fakeFloat, + }); + }); + + it('should set widgets', () => { + const fakeWidgets = [ + { + id: '36141f10-feb6-4c42-acdb-261523729625', + field_name: 'CustomerId', + widget_type: 'Textarea', + widget_params: '', + name: 'Customer', + description: '', + }, + { + id: 'd6a4caa5-68f6-455f-90ff-2ad81856253b', + field_name: 'Price', + widget_type: 'Number', + widget_params: '', + name: '', + description: 'Prices are pointed in USD', + }, + ]; + + component.setWidgets(fakeWidgets); + + expect(component.tableWidgetsList).toEqual(['CustomerId', 'Price']); + expect(component.tableWidgets).toEqual({ + CustomerId: { + id: '36141f10-feb6-4c42-acdb-261523729625', + field_name: 'CustomerId', + widget_type: 'Textarea', + widget_params: null, + name: 'Customer', + description: '', + }, + Price: { + id: 'd6a4caa5-68f6-455f-90ff-2ad81856253b', + field_name: 'Price', + widget_type: 'Number', + widget_params: null, + name: '', + description: 'Prices are pointed in USD', + }, + }); + }); + + it('should return foreign key relations by column name', () => { + component.tableForeignKeys = [ + { + referenced_column_name: 'Id', + referenced_table_name: 'Products', + constraint_name: 'Orders_ibfk_1', + column_name: 'ProductId', + }, + { + referenced_column_name: 'Id', + referenced_table_name: 'Customers', + constraint_name: 'Orders_ibfk_2', + column_name: 'CustomerId', + }, + ]; + + const foreignKeyRelations = component.getRelations('ProductId'); + expect(foreignKeyRelations).toEqual({ + referenced_column_name: 'Id', + referenced_table_name: 'Products', + constraint_name: 'Orders_ibfk_1', + column_name: 'ProductId', + }); + }); + + it('should check if field is readonly', () => { + component.readonlyFields = ['Id', 'Price']; + + const isPriceReafonly = component.isReadonlyField('Price'); + expect(isPriceReafonly).toBe(true); + }); + + it('should check if field is widget', () => { + component.tableWidgetsList = ['CustomerId', 'Price']; + + const isPriceWidget = component.isWidget('Price'); + expect(isPriceWidget).toBe(true); + }); + + describe('updateField for password widget behavior', () => { + beforeEach(() => { + component.tableRowValues = { + id: 1, + username: 'testuser', + password: '***', + }; + }); + + it('should update tableRowValues when password field receives a value', () => { + component.updateField('newPassword', 'password'); + expect(component.tableRowValues.password).toBe('newPassword'); + }); + + it('should update tableRowValues when password field receives empty string', () => { + component.updateField('', 'password'); + expect(component.tableRowValues.password).toBe(''); + }); + + it('should update tableRowValues when password field receives null (clear password)', () => { + component.updateField(null, 'password'); + expect(component.tableRowValues.password).toBe(null); + }); + + it('should handle password field update alongside other fields', () => { + component.updateField('updatedUser', 'username'); + component.updateField('newPassword', 'password'); + + expect(component.tableRowValues.username).toBe('updatedUser'); + expect(component.tableRowValues.password).toBe('newPassword'); + }); + }); + + describe('getFormattedUpdatedRow', () => { + beforeEach(() => { + vi.spyOn(connectionsService, 'currentConnection', 'get').mockReturnValue({ + id: 'test-id', + database: 'test-db', + title: 'Test Connection', + host: 'localhost', + port: '5432', + sid: null, + type: DBtype.Postgres, + username: 'test-user', + ssh: false, + ssl: false, + cert: '', + masterEncryption: false, + azure_encryption: false, + connectionType: ConnectionType.Direct, + } as Connection); + component.tableTypes = {}; + component.nonModifyingFields = []; + component.pageAction = null; + }); + + it('should include password field when it has a value', () => { + component.tableRowValues = { + id: 1, + username: 'testuser', + password: 'newPassword', + }; + + const result = component.getFormattedUpdatedRow(); + expect((result as any).password).toBe('newPassword'); + }); + + it('should include password field when it is null (explicit clear)', () => { + component.tableRowValues = { + id: 1, + username: 'testuser', + password: null, + }; + + const result = component.getFormattedUpdatedRow(); + expect((result as any).password).toBe(null); + }); + + it('should include password field when it is empty string', () => { + component.tableRowValues = { + id: 1, + username: 'testuser', + password: '', + }; + + const result = component.getFormattedUpdatedRow(); + expect((result as any).password).toBe(''); + }); + + it('should preserve other fields when password is empty', () => { + component.tableRowValues = { + id: 1, + username: 'testuser', + password: '', + }; + + const result = component.getFormattedUpdatedRow(); + expect((result as any).id).toBe(1); + expect((result as any).username).toBe('testuser'); + expect((result as any).password).toBe(''); + }); + }); }); diff --git a/frontend/src/app/components/email-change/email-change.component.spec.ts b/frontend/src/app/components/email-change/email-change.component.spec.ts index 55c112501..fd5c7d68f 100644 --- a/frontend/src/app/components/email-change/email-change.component.spec.ts +++ b/frontend/src/app/components/email-change/email-change.component.spec.ts @@ -1,47 +1,42 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { EmailChangeComponent } from './email-change.component'; -import { FormsModule } from '@angular/forms'; -import { UserService } from 'src/app/services/user.service'; -import { of } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { provideHttpClient } from '@angular/common/http'; import { provideRouter } from '@angular/router'; +import { of } from 'rxjs'; +import { UserService } from 'src/app/services/user.service'; +import { EmailChangeComponent } from './email-change.component'; describe('EmailChangeComponent', () => { - let component: EmailChangeComponent; - let fixture: ComponentFixture; - let userService: UserService; + let component: EmailChangeComponent; + let fixture: ComponentFixture; + let userService: UserService; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - FormsModule, - EmailChangeComponent, - BrowserAnimationsModule - ], - providers: [provideHttpClient(), provideRouter([])] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatSnackBarModule, FormsModule, EmailChangeComponent, BrowserAnimationsModule], + providers: [provideHttpClient(), provideRouter([])], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(EmailChangeComponent); - component = fixture.componentInstance; - userService = TestBed.inject(UserService); - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(EmailChangeComponent); + component = fixture.componentInstance; + userService = TestBed.inject(UserService); + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should update email', () => { - component.token = '12345678'; - component.newEmail = 'new@email.com' - const fakeUpdateEmail = vi.spyOn(userService, 'changeEmail').mockReturnValue(of()); + it('should update email', () => { + component.token = '12345678'; + component.newEmail = 'new@email.com'; + const fakeUpdateEmail = vi.spyOn(userService, 'changeEmail').mockReturnValue(of()); - component.updateEmail(); - expect(fakeUpdateEmail).toHaveBeenCalledWith('12345678', 'new@email.com'); - }); -}); \ No newline at end of file + component.updateEmail(); + expect(fakeUpdateEmail).toHaveBeenCalledWith('12345678', 'new@email.com'); + }); +}); diff --git a/frontend/src/app/components/email-verification/email-verification.component.spec.ts b/frontend/src/app/components/email-verification/email-verification.component.spec.ts index 769c98235..5606bbfdd 100644 --- a/frontend/src/app/components/email-verification/email-verification.component.spec.ts +++ b/frontend/src/app/components/email-verification/email-verification.component.spec.ts @@ -1,58 +1,58 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { EmailVerificationComponent } from './email-verification.component'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router'; +import { ActivatedRoute, convertToParamMap, provideRouter, Router } from '@angular/router'; import { of } from 'rxjs'; import { AuthService } from 'src/app/services/auth.service'; -import { Router } from '@angular/router'; -import { provideHttpClient } from '@angular/common/http'; +import { EmailVerificationComponent } from './email-verification.component'; describe('EmailVerificationComponent', () => { - let component: EmailVerificationComponent; - let fixture: ComponentFixture; - let authService: AuthService; - // let mockLocalStorage; - let routerSpy; - - beforeEach(async () => { - routerSpy = {navigate: vi.fn()}; - - await TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - EmailVerificationComponent - ], - providers: [ - provideHttpClient(), - provideRouter([]), - {provide: ActivatedRoute, useValue: { - paramMap: of(convertToParamMap({ - 'verification-token': '1234567890-abcd' - })), - }}, - { provide: Router, useValue: routerSpy }, - ] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(EmailVerificationComponent); - component = fixture.componentInstance; - authService = TestBed.inject(AuthService); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should verify email', async() => { - const fakeVerifyEmail = vi.spyOn(authService, 'verifyEmail').mockReturnValue(of()); - - component.ngOnInit(); - - expect(fakeVerifyEmail).toHaveBeenCalledWith('1234567890-abcd'); - // expect(routerSpy.navigate).toHaveBeenCalledWith(['/user-settings']); - }); + let component: EmailVerificationComponent; + let fixture: ComponentFixture; + let authService: AuthService; + // let mockLocalStorage; + let routerSpy; + + beforeEach(async () => { + routerSpy = { navigate: vi.fn() }; + + await TestBed.configureTestingModule({ + imports: [MatSnackBarModule, EmailVerificationComponent], + providers: [ + provideHttpClient(), + provideRouter([]), + { + provide: ActivatedRoute, + useValue: { + paramMap: of( + convertToParamMap({ + 'verification-token': '1234567890-abcd', + }), + ), + }, + }, + { provide: Router, useValue: routerSpy }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EmailVerificationComponent); + component = fixture.componentInstance; + authService = TestBed.inject(AuthService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should verify email', async () => { + const fakeVerifyEmail = vi.spyOn(authService, 'verifyEmail').mockReturnValue(of()); + + component.ngOnInit(); + + expect(fakeVerifyEmail).toHaveBeenCalledWith('1234567890-abcd'); + // expect(routerSpy.navigate).toHaveBeenCalledWith(['/user-settings']); + }); }); diff --git a/frontend/src/app/components/login/sso-dialog/sso-dialog.component.ts b/frontend/src/app/components/login/sso-dialog/sso-dialog.component.ts index cdf893631..c79fbc4e0 100644 --- a/frontend/src/app/components/login/sso-dialog/sso-dialog.component.ts +++ b/frontend/src/app/components/login/sso-dialog/sso-dialog.component.ts @@ -1,5 +1,5 @@ -import { Component } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDialogModule } from '@angular/material/dialog'; @@ -7,22 +7,15 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; @Component({ - selector: 'app-sso-dialog', - imports: [ - CommonModule, - FormsModule, - MatDialogModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule - ], - templateUrl: './sso-dialog.component.html', - styleUrl: './sso-dialog.component.css' + selector: 'app-sso-dialog', + imports: [CommonModule, FormsModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatButtonModule], + templateUrl: './sso-dialog.component.html', + styleUrl: './sso-dialog.component.css', }) export class SsoDialogComponent { - public companySsoIdentifier: string = ''; + public companySsoIdentifier: string = ''; - loginWithSSO() { - window.location.href = `https://app.rocketadmin.com/saas/saml/login-by-slug/${this.companySsoIdentifier}`; - } + loginWithSSO() { + window.location.href = `https://app.rocketadmin.com/saas/saml/login-by-slug/${this.companySsoIdentifier}`; + } } diff --git a/frontend/src/app/components/password-request/password-request.component.spec.ts b/frontend/src/app/components/password-request/password-request.component.spec.ts index 66f367cbf..88872f7dd 100644 --- a/frontend/src/app/components/password-request/password-request.component.spec.ts +++ b/frontend/src/app/components/password-request/password-request.component.spec.ts @@ -1,50 +1,43 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { PasswordRequestComponent } from './password-request.component'; +import { FormsModule } from '@angular/forms'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { FormsModule } from '@angular/forms'; -import { UserService } from 'src/app/services/user.service'; -import { of } from 'rxjs'; -import { RouterTestingModule } from '@angular/router/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { provideHttpClient } from '@angular/common/http'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; +import { UserService } from 'src/app/services/user.service'; +import { PasswordRequestComponent } from './password-request.component'; describe('PasswordRequestComponent', () => { - let component: PasswordRequestComponent; - let fixture: ComponentFixture; - let userService: UserService; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - RouterTestingModule, - FormsModule, - MatSnackBarModule, - PasswordRequestComponent, - BrowserAnimationsModule - ], - providers: [provideHttpClient()] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(PasswordRequestComponent); - component = fixture.componentInstance; - userService = TestBed.inject(UserService); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should create', () => { - component.userEmail = "eric@cartman.ass"; - component.companyId = "company_1111" - const fakePasswordReset = vi.spyOn(userService, 'requestPasswordReset').mockReturnValue(of()); - - component.requestPassword(); - - expect(fakePasswordReset).toHaveBeenCalledWith("eric@cartman.ass", "company_1111"); - }); + let component: PasswordRequestComponent; + let fixture: ComponentFixture; + let userService: UserService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RouterTestingModule, FormsModule, MatSnackBarModule, PasswordRequestComponent, BrowserAnimationsModule], + providers: [provideHttpClient()], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PasswordRequestComponent); + component = fixture.componentInstance; + userService = TestBed.inject(UserService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create', () => { + component.userEmail = 'eric@cartman.ass'; + component.companyId = 'company_1111'; + const fakePasswordReset = vi.spyOn(userService, 'requestPasswordReset').mockReturnValue(of()); + + component.requestPassword(); + + expect(fakePasswordReset).toHaveBeenCalledWith('eric@cartman.ass', 'company_1111'); + }); }); diff --git a/frontend/src/app/components/registration/registration.component.css b/frontend/src/app/components/registration/registration.component.css index c40b1d8e1..1079fb769 100644 --- a/frontend/src/app/components/registration/registration.component.css +++ b/frontend/src/app/components/registration/registration.component.css @@ -111,8 +111,12 @@ margin-top: 8px; } +.turnstile-widget { + margin-top: 24px; +} + .submit-button { - margin-top: 64px; + margin-top: 40px; } .register-image-box { diff --git a/frontend/src/app/components/registration/registration.component.html b/frontend/src/app/components/registration/registration.component.html index ffa07c28c..294c98871 100644 --- a/frontend/src/app/components/registration/registration.component.html +++ b/frontend/src/app/components/registration/registration.component.html @@ -44,10 +44,17 @@

(onFieldChange)="updatePasswordField($event)"> + + +

diff --git a/frontend/src/app/components/registration/registration.component.spec.ts b/frontend/src/app/components/registration/registration.component.spec.ts index 84f4acf70..58a9db7f8 100644 --- a/frontend/src/app/components/registration/registration.component.spec.ts +++ b/frontend/src/app/components/registration/registration.component.spec.ts @@ -1,5 +1,5 @@ import { provideHttpClient } from '@angular/common/http'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { MatIconTestingModule } from '@angular/material/icon/testing'; import { MatSnackBarModule } from '@angular/material/snack-bar'; @@ -41,6 +41,14 @@ describe('RegistrationComponent', () => { }, }, }; + + // Mock Turnstile + window.turnstile = { + render: vi.fn().mockReturnValue('mock-widget-id'), + reset: vi.fn(), + getResponse: vi.fn(), + remove: vi.fn(), + }; }); beforeEach(() => { @@ -50,11 +58,16 @@ describe('RegistrationComponent', () => { fixture.detectChanges(); }); + afterEach(() => { + delete window.turnstile; + }); + it('should create', () => { expect(component).toBeTruthy(); }); - it('should sign a user in', () => { + it('should sign a user in without turnstile token when not SaaS', () => { + component.isSaas = false; component.user = { email: 'john@smith.com', password: 'kK123456789', @@ -69,4 +82,39 @@ describe('RegistrationComponent', () => { }); expect(component.submitting).toBe(false); }); + + it('should include turnstile token in registration request when SaaS', () => { + component.isSaas = true; + component.user = { + email: 'john@smith.com', + password: 'kK123456789', + }; + component.turnstileToken = 'test-turnstile-token'; + + const fakeSignUpUser = vi.spyOn(authService, 'signUpUser').mockReturnValue(of()); + + component.registerUser(); + expect(fakeSignUpUser).toHaveBeenCalledWith({ + email: 'john@smith.com', + password: 'kK123456789', + turnstileToken: 'test-turnstile-token', + }); + }); + + it('should set turnstileToken when onTurnstileToken is called', () => { + component.onTurnstileToken('new-token'); + expect(component.turnstileToken).toBe('new-token'); + }); + + it('should clear turnstileToken when onTurnstileError is called', () => { + component.turnstileToken = 'existing-token'; + component.onTurnstileError(); + expect(component.turnstileToken).toBeNull(); + }); + + it('should clear turnstileToken when onTurnstileExpired is called', () => { + component.turnstileToken = 'existing-token'; + component.onTurnstileExpired(); + expect(component.turnstileToken).toBeNull(); + }); }); diff --git a/frontend/src/app/components/registration/registration.component.ts b/frontend/src/app/components/registration/registration.component.ts index 59f94cbc5..fb5863bb6 100644 --- a/frontend/src/app/components/registration/registration.component.ts +++ b/frontend/src/app/components/registration/registration.component.ts @@ -1,127 +1,161 @@ -import { AfterViewInit, CUSTOM_ELEMENTS_SCHEMA, Component, NgZone, OnInit } from '@angular/core'; -import { AlertActionType, AlertType } from 'src/app/models/alert'; - -import { AlertComponent } from '../ui-components/alert/alert.component'; -import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; -import { AuthService } from 'src/app/services/auth.service'; import { CommonModule } from '@angular/common'; -import { EmailValidationDirective } from 'src/app/directives/emailValidator.directive'; +import { AfterViewInit, Component, CUSTOM_ELEMENTS_SCHEMA, NgZone, OnInit, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; +import { Router } from '@angular/router'; +import { Angulartics2, Angulartics2OnModule } from 'angulartics2'; +import { EmailValidationDirective } from 'src/app/directives/emailValidator.directive'; +import { AlertActionType, AlertType } from 'src/app/models/alert'; import { NewAuthUser } from 'src/app/models/user'; +import { AuthService } from 'src/app/services/auth.service'; import { NotificationsService } from 'src/app/services/notifications.service'; -import { Router } from '@angular/router'; -import { UserPasswordComponent } from '../ui-components/user-password/user-password.component'; import { environment } from 'src/environments/environment'; +import { AlertComponent } from '../ui-components/alert/alert.component'; +import { TurnstileComponent } from '../ui-components/turnstile/turnstile.component'; +import { UserPasswordComponent } from '../ui-components/user-password/user-password.component'; @Component({ - selector: 'app-registration', - templateUrl: './registration.component.html', - styleUrls: ['./registration.component.css'], - imports: [ - CommonModule, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatButtonModule, - MatIconModule, - EmailValidationDirective, - AlertComponent, - UserPasswordComponent, - Angulartics2OnModule - ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + selector: 'app-registration', + templateUrl: './registration.component.html', + styleUrls: ['./registration.component.css'], + imports: [ + CommonModule, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonModule, + MatIconModule, + EmailValidationDirective, + AlertComponent, + TurnstileComponent, + UserPasswordComponent, + Angulartics2OnModule, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) export class RegistrationComponent implements OnInit, AfterViewInit { + @ViewChild(TurnstileComponent) turnstileWidget: TurnstileComponent; + + public isSaas = (environment as any).saas; + public user: NewAuthUser = { + email: '', + password: '', + }; + public submitting: boolean; + public turnstileToken: string | null = null; + public errors = { + 'User_with_this_email_is_already_registered.': 'User with this email is already registered', + 'GitHub_registration_failed._Please_contact_our_support_team.': + 'GitHub registration failed. Please contact our support team.', + }; + + constructor( + private ngZone: NgZone, + private angulartics2: Angulartics2, + public router: Router, + private _auth: AuthService, + private _notifications: NotificationsService, + ) {} + + ngOnInit(): void { + this.angulartics2.eventTrack.next({ + action: 'Reg: Registration page (component) is loaded', + }); + + const error = new URLSearchParams(location.search).get('error'); + if (error) + this._notifications.showAlert(AlertType.Error, this.errors[error] || error, [ + { + type: AlertActionType.Button, + caption: 'Dismiss', + action: (_id: number) => this._notifications.dismissAlert(), + }, + ]); + } + + ngAfterViewInit() { + //@ts-expect-error + gtag('event', 'conversion', { send_to: 'AW-419937947/auKoCOvwgoYDEJv9nsgB' }); + + //@ts-expect-error + google.accounts.id.initialize({ + client_id: '681163285738-e4l0lrv5vv7m616ucrfhnhso9r396lum.apps.googleusercontent.com', + callback: (authUser) => { + this.ngZone.run(() => { + this._auth.signUpWithGoogle(authUser.credential).subscribe(() => { + this.angulartics2.eventTrack.next({ + action: 'Reg: google register success', + }); + }); + }); + }, + }); + //@ts-expect-error + google.accounts.id.renderButton(document.getElementById('google_registration_button'), { + theme: 'filled_blue', + size: 'large', + width: 400, + text: 'signup_with', + }); + //@ts-expect-error + google.accounts.id.prompt(); + } + + updatePasswordField(updatedValue: string) { + this.user.password = updatedValue; + } + + registerUser() { + this.submitting = true; + + const userData: NewAuthUser = { + ...this.user, + ...(this.isSaas && this.turnstileToken ? { turnstileToken: this.turnstileToken } : {}), + }; + + this._auth.signUpUser(userData).subscribe( + () => { + this.angulartics2.eventTrack.next({ + action: 'Reg: sing up success', + }); + }, + (_error) => { + this.angulartics2.eventTrack.next({ + action: 'Reg: sing up unsuccessful', + }); + this.submitting = false; + this._resetTurnstile(); + }, + () => (this.submitting = false), + ); + } + + onTurnstileToken(token: string) { + this.turnstileToken = token; + } + + onTurnstileError() { + this.turnstileToken = null; + } + + onTurnstileExpired() { + this.turnstileToken = null; + } + + private _resetTurnstile(): void { + if (this.isSaas && this.turnstileWidget) { + this.turnstileWidget.reset(); + this.turnstileToken = null; + } + } - public isSaas = (environment as any).saas; - public user: NewAuthUser = { - email: '', - password: '' - }; - public submitting: boolean; - public errors = { - 'User_with_this_email_is_already_registered.': 'User with this email is already registered', - 'GitHub_registration_failed._Please_contact_our_support_team.': 'GitHub registration failed. Please contact our support team.', - } - - constructor( - private ngZone: NgZone, - private angulartics2: Angulartics2, - public router: Router, - private _auth: AuthService, - private _notifications: NotificationsService, - ) { - } - - ngOnInit(): void { - this.angulartics2.eventTrack.next({ - action: 'Reg: Registration page (component) is loaded' - }); - - const error = new URLSearchParams(location.search).get('error'); - if (error) this._notifications.showAlert(AlertType.Error, this.errors[error] || error, [ - { - type: AlertActionType.Button, - caption: 'Dismiss', - action: (_id: number) => this._notifications.dismissAlert() - } - ]); - } - - ngAfterViewInit() { - //@ts-expect-error - gtag('event', 'conversion', {'send_to': 'AW-419937947/auKoCOvwgoYDEJv9nsgB'}); - - //@ts-expect-error - google.accounts.id.initialize({ - client_id: "681163285738-e4l0lrv5vv7m616ucrfhnhso9r396lum.apps.googleusercontent.com", - callback: (authUser) => { - this.ngZone.run(() => { - this._auth.signUpWithGoogle(authUser.credential).subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Reg: google register success' - }); - }); - }) - } - }); - //@ts-expect-error - google.accounts.id.renderButton( - document.getElementById("google_registration_button"), - { theme: "filled_blue", size: "large", width: 400, text: "signup_with" } - ); - //@ts-expect-error - google.accounts.id.prompt(); - } - - updatePasswordField(updatedValue: string) { - this.user.password = updatedValue; - } - - registerUser() { - this.submitting = true; - - this._auth.signUpUser(this.user) - .subscribe(() => { - this.angulartics2.eventTrack.next({ - action: 'Reg: sing up success' - }); - }, (_error) => { - this.angulartics2.eventTrack.next({ - action: 'Reg: sing up unsuccessful' - }); - this.submitting = false; - }, () => this.submitting = false) - } - - registerWithGithub() { - this._auth.signUpWithGithub(); - this.angulartics2.eventTrack.next({ - action: 'Reg: github register redirect' - }); - } + registerWithGithub() { + this._auth.signUpWithGithub(); + this.angulartics2.eventTrack.next({ + action: 'Reg: github register redirect', + }); + } } diff --git a/frontend/src/app/components/secrets/audit-log-dialog/audit-log-dialog.component.spec.ts b/frontend/src/app/components/secrets/audit-log-dialog/audit-log-dialog.component.spec.ts index c2763a6a6..0098e8f40 100644 --- a/frontend/src/app/components/secrets/audit-log-dialog/audit-log-dialog.component.spec.ts +++ b/frontend/src/app/components/secrets/audit-log-dialog/audit-log-dialog.component.spec.ts @@ -1,312 +1,308 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { PageEvent } from '@angular/material/paginator'; import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Angulartics2Module } from 'angulartics2'; import { of } from 'rxjs'; -import { PageEvent } from '@angular/material/paginator'; - -import { AuditLogDialogComponent } from './audit-log-dialog.component'; +import { AuditLogEntry, AuditLogResponse, Secret } from 'src/app/models/secret'; import { SecretsService } from 'src/app/services/secrets.service'; -import { Secret, AuditLogEntry, AuditLogResponse } from 'src/app/models/secret'; +import { AuditLogDialogComponent } from './audit-log-dialog.component'; describe('AuditLogDialogComponent', () => { - let component: AuditLogDialogComponent; - let fixture: ComponentFixture; - let mockSecretsService: { getAuditLog: ReturnType }; - let mockDialogRef: { close: ReturnType }; - - const mockSecret: Secret = { - id: '1', - slug: 'test-secret', - companyId: '1', - createdAt: '2024-01-01', - updatedAt: '2024-01-01', - masterEncryption: false, - }; - - const mockAuditLogEntry: AuditLogEntry = { - id: '1', - action: 'create', - user: { id: '1', email: 'user@example.com' }, - accessedAt: '2024-01-01T00:00:00Z', - success: true, - }; - - const createMockAuditLogResponse = (): AuditLogResponse => ({ - data: [mockAuditLogEntry], - pagination: { total: 1, currentPage: 1, perPage: 20, lastPage: 1 }, - }); - - const createMockMultipleLogsResponse = (): AuditLogResponse => ({ - data: [ - mockAuditLogEntry, - { - id: '2', - action: 'view', - user: { id: '2', email: 'viewer@example.com' }, - accessedAt: '2024-01-02T00:00:00Z', - success: true, - }, - { - id: '3', - action: 'update', - user: { id: '1', email: 'user@example.com' }, - accessedAt: '2024-01-03T00:00:00Z', - success: true, - }, - { - id: '4', - action: 'copy', - user: { id: '3', email: 'copier@example.com' }, - accessedAt: '2024-01-04T00:00:00Z', - success: true, - }, - { - id: '5', - action: 'delete', - user: { id: '1', email: 'user@example.com' }, - accessedAt: '2024-01-05T00:00:00Z', - success: false, - errorMessage: 'Deletion failed', - }, - ], - pagination: { total: 5, currentPage: 1, perPage: 20, lastPage: 1 }, - }); - - beforeEach(async () => { - mockSecretsService = { - getAuditLog: vi.fn().mockImplementation(() => of(createMockAuditLogResponse())) - }; - - mockDialogRef = { close: vi.fn() }; - - await TestBed.configureTestingModule({ - imports: [ - AuditLogDialogComponent, - BrowserAnimationsModule, - MatSnackBarModule, - Angulartics2Module.forRoot(), - ], - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - { provide: SecretsService, useValue: mockSecretsService }, - { provide: MatDialogRef, useValue: mockDialogRef }, - { provide: MAT_DIALOG_DATA, useValue: { secret: mockSecret } }, - ] - }).compileComponents(); - - fixture = TestBed.createComponent(AuditLogDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('component initialization', () => { - it('should load audit log on init', () => { - expect(mockSecretsService.getAuditLog).toHaveBeenCalledWith('test-secret', 1, 20); - }); - - it('should display audit log entries', () => { - expect(component.logs.length).toBe(1); - expect(component.logs[0].action).toBe('create'); - }); - - it('should initialize with default pagination', () => { - expect(component.pagination).toEqual({ - total: 1, - currentPage: 1, - perPage: 20, - lastPage: 1 - }); - }); - - it('should initialize loading as false after load', () => { - expect(component.loading).toBe(false); - }); - - it('should have correct displayed columns', () => { - expect(component.displayedColumns).toEqual(['action', 'user', 'accessedAt', 'success']); - }); - }); - - describe('action labels', () => { - it('should have label for create action', () => { - expect(component.actionLabels.create).toBe('Created'); - }); - - it('should have label for view action', () => { - expect(component.actionLabels.view).toBe('Viewed'); - }); - - it('should have label for copy action', () => { - expect(component.actionLabels.copy).toBe('Copied'); - }); - - it('should have label for update action', () => { - expect(component.actionLabels.update).toBe('Updated'); - }); - - it('should have label for delete action', () => { - expect(component.actionLabels.delete).toBe('Deleted'); - }); - }); - - describe('action icons', () => { - it('should have icon for create action', () => { - expect(component.actionIcons.create).toBe('add_circle'); - }); - - it('should have icon for view action', () => { - expect(component.actionIcons.view).toBe('visibility'); - }); - - it('should have icon for copy action', () => { - expect(component.actionIcons.copy).toBe('content_copy'); - }); - - it('should have icon for update action', () => { - expect(component.actionIcons.update).toBe('edit'); - }); - - it('should have icon for delete action', () => { - expect(component.actionIcons.delete).toBe('delete'); - }); - }); - - describe('action colors', () => { - it('should have color for create action', () => { - expect(component.actionColors.create).toBe('primary'); - }); - - it('should have color for view action', () => { - expect(component.actionColors.view).toBe('accent'); - }); - - it('should have color for copy action', () => { - expect(component.actionColors.copy).toBe('accent'); - }); - - it('should have color for update action', () => { - expect(component.actionColors.update).toBe('primary'); - }); - - it('should have color for delete action', () => { - expect(component.actionColors.delete).toBe('warn'); - }); - }); - - describe('loadAuditLog', () => { - it('should set loading to true while fetching', () => { - component.loading = false; - mockSecretsService.getAuditLog.mockImplementation(() => of(createMockAuditLogResponse())); - - component.loadAuditLog(); - - expect(component.loading).toBe(false); - }); - - it('should update logs on successful fetch', () => { - mockSecretsService.getAuditLog.mockImplementation(() => of(createMockMultipleLogsResponse())); - - component.loadAuditLog(); - - expect(component.logs.length).toBe(5); - }); - - it('should update pagination on successful fetch', () => { - mockSecretsService.getAuditLog.mockImplementation(() => of(createMockMultipleLogsResponse())); - - component.loadAuditLog(); - - expect(component.pagination.total).toBe(5); - }); - - it('should call getAuditLog with current pagination', () => { - mockSecretsService.getAuditLog.mockClear(); - component.pagination.currentPage = 2; - component.pagination.perPage = 10; - - component.loadAuditLog(); - - expect(mockSecretsService.getAuditLog).toHaveBeenCalledWith('test-secret', 2, 10); - }); - }); - - describe('onPageChange', () => { - it('should update pagination and reload audit log', () => { - const pageEvent: PageEvent = { - pageIndex: 2, - pageSize: 50, - length: 100 - }; - - // Update mock to return pagination matching the page change - mockSecretsService.getAuditLog.mockImplementation(() => of({ - data: [mockAuditLogEntry], - pagination: { total: 100, currentPage: 3, perPage: 50, lastPage: 2 } - })); - mockSecretsService.getAuditLog.mockClear(); - component.onPageChange(pageEvent); - - expect(component.pagination.currentPage).toBe(3); - expect(component.pagination.perPage).toBe(50); - expect(mockSecretsService.getAuditLog).toHaveBeenCalledWith('test-secret', 3, 50); - }); - - it('should handle first page correctly', () => { - const pageEvent: PageEvent = { - pageIndex: 0, - pageSize: 20, - length: 100 - }; - - mockSecretsService.getAuditLog.mockClear(); - component.onPageChange(pageEvent); - - expect(component.pagination.currentPage).toBe(1); - }); - }); - - describe('with multiple audit log entries', () => { - beforeEach(() => { - mockSecretsService.getAuditLog.mockImplementation(() => of(createMockMultipleLogsResponse())); - component.loadAuditLog(); - }); - - it('should display all entries', () => { - expect(component.logs.length).toBe(5); - }); - - it('should include failed actions', () => { - const failedAction = component.logs.find(log => !log.success); - expect(failedAction).toBeTruthy(); - expect(failedAction?.errorMessage).toBe('Deletion failed'); - }); - - it('should include all action types', () => { - const actions = component.logs.map(log => log.action); - expect(actions).toContain('create'); - expect(actions).toContain('view'); - expect(actions).toContain('update'); - expect(actions).toContain('copy'); - expect(actions).toContain('delete'); - }); - }); - - describe('data binding', () => { - it('should have access to secret data', () => { - expect(component.data.secret).toEqual(mockSecret); - }); - - it('should have access to secret slug', () => { - expect(component.data.secret.slug).toBe('test-secret'); - }); - }); + let component: AuditLogDialogComponent; + let fixture: ComponentFixture; + let mockSecretsService: { getAuditLog: ReturnType }; + let mockDialogRef: { close: ReturnType }; + + const mockSecret: Secret = { + id: '1', + slug: 'test-secret', + companyId: '1', + createdAt: '2024-01-01', + updatedAt: '2024-01-01', + masterEncryption: false, + }; + + const mockAuditLogEntry: AuditLogEntry = { + id: '1', + action: 'create', + user: { id: '1', email: 'user@example.com' }, + accessedAt: '2024-01-01T00:00:00Z', + success: true, + }; + + const createMockAuditLogResponse = (): AuditLogResponse => ({ + data: [mockAuditLogEntry], + pagination: { total: 1, currentPage: 1, perPage: 20, lastPage: 1 }, + }); + + const createMockMultipleLogsResponse = (): AuditLogResponse => ({ + data: [ + mockAuditLogEntry, + { + id: '2', + action: 'view', + user: { id: '2', email: 'viewer@example.com' }, + accessedAt: '2024-01-02T00:00:00Z', + success: true, + }, + { + id: '3', + action: 'update', + user: { id: '1', email: 'user@example.com' }, + accessedAt: '2024-01-03T00:00:00Z', + success: true, + }, + { + id: '4', + action: 'copy', + user: { id: '3', email: 'copier@example.com' }, + accessedAt: '2024-01-04T00:00:00Z', + success: true, + }, + { + id: '5', + action: 'delete', + user: { id: '1', email: 'user@example.com' }, + accessedAt: '2024-01-05T00:00:00Z', + success: false, + errorMessage: 'Deletion failed', + }, + ], + pagination: { total: 5, currentPage: 1, perPage: 20, lastPage: 1 }, + }); + + beforeEach(async () => { + mockSecretsService = { + getAuditLog: vi.fn().mockImplementation(() => of(createMockAuditLogResponse())), + }; + + mockDialogRef = { close: vi.fn() }; + + await TestBed.configureTestingModule({ + imports: [AuditLogDialogComponent, BrowserAnimationsModule, MatSnackBarModule, Angulartics2Module.forRoot()], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { provide: SecretsService, useValue: mockSecretsService }, + { provide: MatDialogRef, useValue: mockDialogRef }, + { provide: MAT_DIALOG_DATA, useValue: { secret: mockSecret } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AuditLogDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('component initialization', () => { + it('should load audit log on init', () => { + expect(mockSecretsService.getAuditLog).toHaveBeenCalledWith('test-secret', 1, 20); + }); + + it('should display audit log entries', () => { + expect(component.logs.length).toBe(1); + expect(component.logs[0].action).toBe('create'); + }); + + it('should initialize with default pagination', () => { + expect(component.pagination).toEqual({ + total: 1, + currentPage: 1, + perPage: 20, + lastPage: 1, + }); + }); + + it('should initialize loading as false after load', () => { + expect(component.loading).toBe(false); + }); + + it('should have correct displayed columns', () => { + expect(component.displayedColumns).toEqual(['action', 'user', 'accessedAt', 'success']); + }); + }); + + describe('action labels', () => { + it('should have label for create action', () => { + expect(component.actionLabels.create).toBe('Created'); + }); + + it('should have label for view action', () => { + expect(component.actionLabels.view).toBe('Viewed'); + }); + + it('should have label for copy action', () => { + expect(component.actionLabels.copy).toBe('Copied'); + }); + + it('should have label for update action', () => { + expect(component.actionLabels.update).toBe('Updated'); + }); + + it('should have label for delete action', () => { + expect(component.actionLabels.delete).toBe('Deleted'); + }); + }); + + describe('action icons', () => { + it('should have icon for create action', () => { + expect(component.actionIcons.create).toBe('add_circle'); + }); + + it('should have icon for view action', () => { + expect(component.actionIcons.view).toBe('visibility'); + }); + + it('should have icon for copy action', () => { + expect(component.actionIcons.copy).toBe('content_copy'); + }); + + it('should have icon for update action', () => { + expect(component.actionIcons.update).toBe('edit'); + }); + + it('should have icon for delete action', () => { + expect(component.actionIcons.delete).toBe('delete'); + }); + }); + + describe('action colors', () => { + it('should have color for create action', () => { + expect(component.actionColors.create).toBe('primary'); + }); + + it('should have color for view action', () => { + expect(component.actionColors.view).toBe('accent'); + }); + + it('should have color for copy action', () => { + expect(component.actionColors.copy).toBe('accent'); + }); + + it('should have color for update action', () => { + expect(component.actionColors.update).toBe('primary'); + }); + + it('should have color for delete action', () => { + expect(component.actionColors.delete).toBe('warn'); + }); + }); + + describe('loadAuditLog', () => { + it('should set loading to true while fetching', () => { + component.loading = false; + mockSecretsService.getAuditLog.mockImplementation(() => of(createMockAuditLogResponse())); + + component.loadAuditLog(); + + expect(component.loading).toBe(false); + }); + + it('should update logs on successful fetch', () => { + mockSecretsService.getAuditLog.mockImplementation(() => of(createMockMultipleLogsResponse())); + + component.loadAuditLog(); + + expect(component.logs.length).toBe(5); + }); + + it('should update pagination on successful fetch', () => { + mockSecretsService.getAuditLog.mockImplementation(() => of(createMockMultipleLogsResponse())); + + component.loadAuditLog(); + + expect(component.pagination.total).toBe(5); + }); + + it('should call getAuditLog with current pagination', () => { + mockSecretsService.getAuditLog.mockClear(); + component.pagination.currentPage = 2; + component.pagination.perPage = 10; + + component.loadAuditLog(); + + expect(mockSecretsService.getAuditLog).toHaveBeenCalledWith('test-secret', 2, 10); + }); + }); + + describe('onPageChange', () => { + it('should update pagination and reload audit log', () => { + const pageEvent: PageEvent = { + pageIndex: 2, + pageSize: 50, + length: 100, + }; + + // Update mock to return pagination matching the page change + mockSecretsService.getAuditLog.mockImplementation(() => + of({ + data: [mockAuditLogEntry], + pagination: { total: 100, currentPage: 3, perPage: 50, lastPage: 2 }, + }), + ); + mockSecretsService.getAuditLog.mockClear(); + component.onPageChange(pageEvent); + + expect(component.pagination.currentPage).toBe(3); + expect(component.pagination.perPage).toBe(50); + expect(mockSecretsService.getAuditLog).toHaveBeenCalledWith('test-secret', 3, 50); + }); + + it('should handle first page correctly', () => { + const pageEvent: PageEvent = { + pageIndex: 0, + pageSize: 20, + length: 100, + }; + + mockSecretsService.getAuditLog.mockClear(); + component.onPageChange(pageEvent); + + expect(component.pagination.currentPage).toBe(1); + }); + }); + + describe('with multiple audit log entries', () => { + beforeEach(() => { + mockSecretsService.getAuditLog.mockImplementation(() => of(createMockMultipleLogsResponse())); + component.loadAuditLog(); + }); + + it('should display all entries', () => { + expect(component.logs.length).toBe(5); + }); + + it('should include failed actions', () => { + const failedAction = component.logs.find((log) => !log.success); + expect(failedAction).toBeTruthy(); + expect(failedAction?.errorMessage).toBe('Deletion failed'); + }); + + it('should include all action types', () => { + const actions = component.logs.map((log) => log.action); + expect(actions).toContain('create'); + expect(actions).toContain('view'); + expect(actions).toContain('update'); + expect(actions).toContain('copy'); + expect(actions).toContain('delete'); + }); + }); + + describe('data binding', () => { + it('should have access to secret data', () => { + expect(component.data.secret).toEqual(mockSecret); + }); + + it('should have access to secret slug', () => { + expect(component.data.secret.slug).toBe('test-secret'); + }); + }); }); diff --git a/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.spec.ts b/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.spec.ts index 73e1fb9b0..7adf1b437 100644 --- a/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.spec.ts +++ b/frontend/src/app/components/secrets/create-secret-dialog/create-secret-dialog.component.spec.ts @@ -27,11 +27,11 @@ describe('CreateSecretDialogComponent', () => { beforeEach(async () => { mockSecretsService = { - createSecret: vi.fn().mockReturnValue(of(mockSecret)) + createSecret: vi.fn().mockReturnValue(of(mockSecret)), }; mockDialogRef = { - close: vi.fn() + close: vi.fn(), }; await TestBed.configureTestingModule({ diff --git a/frontend/src/app/components/secrets/delete-secret-dialog/delete-secret-dialog.component.spec.ts b/frontend/src/app/components/secrets/delete-secret-dialog/delete-secret-dialog.component.spec.ts index a6080aa87..faa96b852 100644 --- a/frontend/src/app/components/secrets/delete-secret-dialog/delete-secret-dialog.component.spec.ts +++ b/frontend/src/app/components/secrets/delete-secret-dialog/delete-secret-dialog.component.spec.ts @@ -1,154 +1,148 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Angulartics2Module } from 'angulartics2'; import { of, throwError } from 'rxjs'; - -import { DeleteSecretDialogComponent } from './delete-secret-dialog.component'; +import { DeleteSecretResponse, Secret } from 'src/app/models/secret'; import { SecretsService } from 'src/app/services/secrets.service'; -import { Secret, DeleteSecretResponse } from 'src/app/models/secret'; +import { DeleteSecretDialogComponent } from './delete-secret-dialog.component'; describe('DeleteSecretDialogComponent', () => { - let component: DeleteSecretDialogComponent; - let fixture: ComponentFixture; - let mockSecretsService: { deleteSecret: ReturnType }; - let mockDialogRef: { close: ReturnType }; - - const mockSecret: Secret = { - id: '1', - slug: 'test-secret', - companyId: '1', - createdAt: '2024-01-01', - updatedAt: '2024-01-01', - masterEncryption: false, - }; - - const mockSecretWithEncryption: Secret = { - ...mockSecret, - masterEncryption: true, - }; - - const mockDeleteResponse: DeleteSecretResponse = { - message: 'Secret deleted successfully', - deletedAt: '2024-01-01T00:00:00Z', - }; - - const createComponent = async (secret: Secret = mockSecret) => { - mockSecretsService = { - deleteSecret: vi.fn().mockReturnValue(of(mockDeleteResponse)) - }; - - mockDialogRef = { close: vi.fn() }; - - await TestBed.configureTestingModule({ - imports: [ - DeleteSecretDialogComponent, - BrowserAnimationsModule, - MatSnackBarModule, - Angulartics2Module.forRoot(), - ], - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - { provide: SecretsService, useValue: mockSecretsService }, - { provide: MatDialogRef, useValue: mockDialogRef }, - { provide: MAT_DIALOG_DATA, useValue: { secret } }, - ] - }).compileComponents(); - - fixture = TestBed.createComponent(DeleteSecretDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }; - - beforeEach(async () => { - await createComponent(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('component initialization', () => { - it('should initialize with submitting false', () => { - expect(component.submitting).toBe(false); - }); - - it('should have access to secret data', () => { - expect(component.data.secret).toEqual(mockSecret); - }); - - it('should have access to secret slug', () => { - expect(component.data.secret.slug).toBe('test-secret'); - }); - }); - - describe('onDelete', () => { - it('should call deleteSecret with correct slug', () => { - component.onDelete(); - expect(mockSecretsService.deleteSecret).toHaveBeenCalledWith('test-secret'); - }); - - it('should set submitting to true during deletion', () => { - expect(component.submitting).toBe(false); - component.onDelete(); - }); - - it('should close dialog with true after successful deletion', () => { - component.onDelete(); - expect(mockDialogRef.close).toHaveBeenCalledWith(true); - }); - - it('should reset submitting after successful deletion', () => { - component.onDelete(); - expect(component.submitting).toBe(false); - }); - - it('should reset submitting on error', () => { - mockSecretsService.deleteSecret.mockReturnValue(throwError(() => new Error('Error'))); - - component.onDelete(); - - expect(component.submitting).toBe(false); - }); - - it('should not close dialog on error', () => { - mockSecretsService.deleteSecret.mockReturnValue(throwError(() => new Error('Error'))); - - component.onDelete(); - - expect(mockDialogRef.close).not.toHaveBeenCalled(); - }); - }); - - describe('with encrypted secret', () => { - beforeEach(async () => { - await TestBed.resetTestingModule(); - await createComponent(mockSecretWithEncryption); - }); - - it('should have access to encrypted secret data', () => { - expect(component.data.secret.masterEncryption).toBe(true); - }); - - it('should delete encrypted secret with correct slug', () => { - component.onDelete(); - expect(mockSecretsService.deleteSecret).toHaveBeenCalledWith('test-secret'); - }); - }); - - describe('dialog interactions', () => { - it('should only call deleteSecret once per click', () => { - component.onDelete(); - expect(mockSecretsService.deleteSecret).toHaveBeenCalledTimes(1); - }); - - it('should close dialog only once after successful deletion', () => { - component.onDelete(); - expect(mockDialogRef.close).toHaveBeenCalledTimes(1); - }); - }); + let component: DeleteSecretDialogComponent; + let fixture: ComponentFixture; + let mockSecretsService: { deleteSecret: ReturnType }; + let mockDialogRef: { close: ReturnType }; + + const mockSecret: Secret = { + id: '1', + slug: 'test-secret', + companyId: '1', + createdAt: '2024-01-01', + updatedAt: '2024-01-01', + masterEncryption: false, + }; + + const mockSecretWithEncryption: Secret = { + ...mockSecret, + masterEncryption: true, + }; + + const mockDeleteResponse: DeleteSecretResponse = { + message: 'Secret deleted successfully', + deletedAt: '2024-01-01T00:00:00Z', + }; + + const createComponent = async (secret: Secret = mockSecret) => { + mockSecretsService = { + deleteSecret: vi.fn().mockReturnValue(of(mockDeleteResponse)), + }; + + mockDialogRef = { close: vi.fn() }; + + await TestBed.configureTestingModule({ + imports: [DeleteSecretDialogComponent, BrowserAnimationsModule, MatSnackBarModule, Angulartics2Module.forRoot()], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { provide: SecretsService, useValue: mockSecretsService }, + { provide: MatDialogRef, useValue: mockDialogRef }, + { provide: MAT_DIALOG_DATA, useValue: { secret } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DeleteSecretDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }; + + beforeEach(async () => { + await createComponent(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('component initialization', () => { + it('should initialize with submitting false', () => { + expect(component.submitting).toBe(false); + }); + + it('should have access to secret data', () => { + expect(component.data.secret).toEqual(mockSecret); + }); + + it('should have access to secret slug', () => { + expect(component.data.secret.slug).toBe('test-secret'); + }); + }); + + describe('onDelete', () => { + it('should call deleteSecret with correct slug', () => { + component.onDelete(); + expect(mockSecretsService.deleteSecret).toHaveBeenCalledWith('test-secret'); + }); + + it('should set submitting to true during deletion', () => { + expect(component.submitting).toBe(false); + component.onDelete(); + }); + + it('should close dialog with true after successful deletion', () => { + component.onDelete(); + expect(mockDialogRef.close).toHaveBeenCalledWith(true); + }); + + it('should reset submitting after successful deletion', () => { + component.onDelete(); + expect(component.submitting).toBe(false); + }); + + it('should reset submitting on error', () => { + mockSecretsService.deleteSecret.mockReturnValue(throwError(() => new Error('Error'))); + + component.onDelete(); + + expect(component.submitting).toBe(false); + }); + + it('should not close dialog on error', () => { + mockSecretsService.deleteSecret.mockReturnValue(throwError(() => new Error('Error'))); + + component.onDelete(); + + expect(mockDialogRef.close).not.toHaveBeenCalled(); + }); + }); + + describe('with encrypted secret', () => { + beforeEach(async () => { + await TestBed.resetTestingModule(); + await createComponent(mockSecretWithEncryption); + }); + + it('should have access to encrypted secret data', () => { + expect(component.data.secret.masterEncryption).toBe(true); + }); + + it('should delete encrypted secret with correct slug', () => { + component.onDelete(); + expect(mockSecretsService.deleteSecret).toHaveBeenCalledWith('test-secret'); + }); + }); + + describe('dialog interactions', () => { + it('should only call deleteSecret once per click', () => { + component.onDelete(); + expect(mockSecretsService.deleteSecret).toHaveBeenCalledTimes(1); + }); + + it('should close dialog only once after successful deletion', () => { + component.onDelete(); + expect(mockDialogRef.close).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/frontend/src/app/components/secrets/edit-secret-dialog/edit-secret-dialog.component.spec.ts b/frontend/src/app/components/secrets/edit-secret-dialog/edit-secret-dialog.component.spec.ts index 53c168532..4c5d2f14f 100644 --- a/frontend/src/app/components/secrets/edit-secret-dialog/edit-secret-dialog.component.spec.ts +++ b/frontend/src/app/components/secrets/edit-secret-dialog/edit-secret-dialog.component.spec.ts @@ -37,7 +37,7 @@ describe('EditSecretDialogComponent', () => { const createComponent = async (secret: Secret = mockSecret) => { mockSecretsService = { - updateSecret: vi.fn().mockReturnValue(of(secret)) + updateSecret: vi.fn().mockReturnValue(of(secret)), }; mockDialogRef = { close: vi.fn() }; diff --git a/frontend/src/app/components/secrets/master-password-dialog/master-password-dialog.component.spec.ts b/frontend/src/app/components/secrets/master-password-dialog/master-password-dialog.component.spec.ts index 792bdbaa5..9e293aef2 100644 --- a/frontend/src/app/components/secrets/master-password-dialog/master-password-dialog.component.spec.ts +++ b/frontend/src/app/components/secrets/master-password-dialog/master-password-dialog.component.spec.ts @@ -1,50 +1,45 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MatDialogRef } from '@angular/material/dialog'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MasterPasswordDialogComponent } from './master-password-dialog.component'; describe('MasterPasswordDialogComponent', () => { - let component: MasterPasswordDialogComponent; - let fixture: ComponentFixture; - let mockDialogRef: { close: ReturnType }; - - beforeEach(async () => { - mockDialogRef = { close: vi.fn() }; - - await TestBed.configureTestingModule({ - imports: [ - MasterPasswordDialogComponent, - BrowserAnimationsModule, - ], - providers: [ - { provide: MatDialogRef, useValue: mockDialogRef }, - ] - }).compileComponents(); - - fixture = TestBed.createComponent(MasterPasswordDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should toggle password visibility', () => { - expect(component.showPassword).toBe(false); - component.togglePasswordVisibility(); - expect(component.showPassword).toBe(true); - }); - - it('should show error when submitting empty password', () => { - component.onSubmit(); - expect(component.error).toBe('Please enter the master password'); - }); - - it('should close dialog with password when submitting valid password', () => { - component.masterPassword = 'testpassword'; - component.onSubmit(); - expect(mockDialogRef.close).toHaveBeenCalledWith('testpassword'); - }); + let component: MasterPasswordDialogComponent; + let fixture: ComponentFixture; + let mockDialogRef: { close: ReturnType }; + + beforeEach(async () => { + mockDialogRef = { close: vi.fn() }; + + await TestBed.configureTestingModule({ + imports: [MasterPasswordDialogComponent, BrowserAnimationsModule], + providers: [{ provide: MatDialogRef, useValue: mockDialogRef }], + }).compileComponents(); + + fixture = TestBed.createComponent(MasterPasswordDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should toggle password visibility', () => { + expect(component.showPassword).toBe(false); + component.togglePasswordVisibility(); + expect(component.showPassword).toBe(true); + }); + + it('should show error when submitting empty password', () => { + component.onSubmit(); + expect(component.error).toBe('Please enter the master password'); + }); + + it('should close dialog with password when submitting valid password', () => { + component.masterPassword = 'testpassword'; + component.onSubmit(); + expect(mockDialogRef.close).toHaveBeenCalledWith('testpassword'); + }); }); diff --git a/frontend/src/app/components/sso/sso.component.ts b/frontend/src/app/components/sso/sso.component.ts index 35077b5da..8853a4fba 100644 --- a/frontend/src/app/components/sso/sso.component.ts +++ b/frontend/src/app/components/sso/sso.component.ts @@ -1,8 +1,5 @@ -import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { Router, RouterModule } from '@angular/router'; - -import { CompanyService } from 'src/app/services/company.service'; +import { Component, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -10,80 +7,92 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { Router, RouterModule } from '@angular/router'; import { SamlConfig } from 'src/app/models/company'; +import { CompanyService } from 'src/app/services/company.service'; @Component({ - selector: 'app-sso', - imports: [ - CommonModule, - MatInputModule, - MatCheckboxModule, - MatIconModule, - MatButtonModule, - MatTooltipModule, - FormsModule, - RouterModule, - MatFormFieldModule - ], - templateUrl: './sso.component.html', - styleUrl: './sso.component.css' + selector: 'app-sso', + imports: [ + CommonModule, + MatInputModule, + MatCheckboxModule, + MatIconModule, + MatButtonModule, + MatTooltipModule, + FormsModule, + RouterModule, + MatFormFieldModule, + ], + templateUrl: './sso.component.html', + styleUrl: './sso.component.css', }) export class SsoComponent implements OnInit { + public companyId: string; - public companyId: string; - - public samlConfigInitial: SamlConfig = { - name: '', - entryPoint: '', - issuer: '', - callbackUrl: '', - cert: '', - signatureAlgorithm: '', - digestAlgorithm: "sha256", - active: true, - authnResponseSignedValidation: false, - assertionsSignedValidation: false, - allowedDomains: [], - displayName: '', - logoUrl: '', - expectedIssuer: '', - slug: '' - }; - public samlConfig: SamlConfig = this.samlConfigInitial; - - public submitting: boolean = false; + public samlConfigInitial: SamlConfig = { + name: '', + entryPoint: '', + issuer: '', + callbackUrl: '', + cert: '', + signatureAlgorithm: '', + digestAlgorithm: 'sha256', + active: true, + authnResponseSignedValidation: false, + assertionsSignedValidation: false, + allowedDomains: [], + displayName: '', + logoUrl: '', + expectedIssuer: '', + slug: '', + }; + public samlConfig: SamlConfig = this.samlConfigInitial; - constructor( - private router: Router, - private _company: CompanyService - ) { } + public submitting: boolean = false; - ngOnInit() { - this.companyId = this.router.routerState.snapshot.root.firstChild.params['company-id']; + constructor( + private router: Router, + private _company: CompanyService, + ) {} - this._company.fetchSamlConfiguration(this.companyId).subscribe( (config) => { - if (config.length) this.samlConfig = config[0]; - }); - } + ngOnInit() { + this.companyId = this.router.routerState.snapshot.root.firstChild.params['company-id']; - createSamlConfiguration() { - this.submitting = true; - this._company.createSamlConfiguration(this.companyId, this.samlConfig).subscribe(() => { - this.submitting = false; - this.router.navigate(['/company']); - }, - () => { this.submitting = false; }, - () => { this.submitting = false; }); - } + this._company.fetchSamlConfiguration(this.companyId).subscribe((config) => { + if (config.length) this.samlConfig = config[0]; + }); + } - updateSamlConfiguration() { - this.submitting = true; - this._company.updateSamlConfiguration(this.samlConfig).subscribe(() => { - this.submitting = false; - this.router.navigate(['/company']); - }, - () => { this.submitting = false; }, - () => { this.submitting = false; }); - } + createSamlConfiguration() { + this.submitting = true; + this._company.createSamlConfiguration(this.companyId, this.samlConfig).subscribe( + () => { + this.submitting = false; + this.router.navigate(['/company']); + }, + () => { + this.submitting = false; + }, + () => { + this.submitting = false; + }, + ); + } + updateSamlConfiguration() { + this.submitting = true; + this._company.updateSamlConfiguration(this.samlConfig).subscribe( + () => { + this.submitting = false; + this.router.navigate(['/company']); + }, + () => { + this.submitting = false; + }, + () => { + this.submitting = false; + }, + ); + } } diff --git a/frontend/src/app/components/ui-components/alert/alert.component.spec.ts b/frontend/src/app/components/ui-components/alert/alert.component.spec.ts index a681d63b6..df35bb32a 100644 --- a/frontend/src/app/components/ui-components/alert/alert.component.spec.ts +++ b/frontend/src/app/components/ui-components/alert/alert.component.spec.ts @@ -1,48 +1,43 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AlertComponent } from './alert.component'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { AlertType, AlertActionType } from 'src/app/models/alert'; +import { AlertActionType, AlertType } from 'src/app/models/alert'; +import { AlertComponent } from './alert.component'; describe('AlertComponent', () => { - let component: AlertComponent; - let fixture: ComponentFixture; + let component: AlertComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - AlertComponent - ] -}) - .compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatSnackBarModule, AlertComponent], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(AlertComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(AlertComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should call finction from action on alert button click', () => { - const buttonAction = vi.fn(); - const alert = { - id: 0, - type: AlertType.Error, - message: 'Error message', - actions: [ - { - type: AlertActionType.Button, - caption: 'Dissmis', - action: buttonAction - } - ] - } - component.onButtonClick(alert, alert.actions[0]); - expect(buttonAction).toHaveBeenCalled(); - }) + it('should call finction from action on alert button click', () => { + const buttonAction = vi.fn(); + const alert = { + id: 0, + type: AlertType.Error, + message: 'Error message', + actions: [ + { + type: AlertActionType.Button, + caption: 'Dissmis', + action: buttonAction, + }, + ], + }; + component.onButtonClick(alert, alert.actions[0]); + expect(buttonAction).toHaveBeenCalled(); + }); }); diff --git a/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.spec.ts b/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.spec.ts index dce0772c5..89daacb59 100644 --- a/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.spec.ts +++ b/frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.spec.ts @@ -1,51 +1,50 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DateTimeFilterComponent } from './date-time.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { DateTimeFilterComponent } from './date-time.component'; describe('DateTimeFilterComponent', () => { - let component: DateTimeFilterComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DateTimeFilterComponent, BrowserAnimationsModule] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DateTimeFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should prepare date and time for date and time inputs', () => { - component.value = '2021-06-26T07:22:00.603'; - component.ngOnInit(); - - expect(component.date).toEqual('2021-06-26'); - expect(component.time).toEqual('07:22:00'); - }); - - it('should send onChange event with new date value', () => { - component.date = '2021-08-26'; - component.time = '07:22:00'; - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onDateChange(); - - expect(event).toHaveBeenCalledWith('2021-08-26T07:22:00Z'); - }); - - it('should send onChange event with new time value', () => { - component.date = '2021-07-26'; - component.time = '07:20:00'; - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onTimeChange(); - - expect(event).toHaveBeenCalledWith('2021-07-26T07:20:00Z'); - }); + let component: DateTimeFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DateTimeFilterComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DateTimeFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should prepare date and time for date and time inputs', () => { + component.value = '2021-06-26T07:22:00.603'; + component.ngOnInit(); + + expect(component.date).toEqual('2021-06-26'); + expect(component.time).toEqual('07:22:00'); + }); + + it('should send onChange event with new date value', () => { + component.date = '2021-08-26'; + component.time = '07:22:00'; + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onDateChange(); + + expect(event).toHaveBeenCalledWith('2021-08-26T07:22:00Z'); + }); + + it('should send onChange event with new time value', () => { + component.date = '2021-07-26'; + component.time = '07:20:00'; + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onTimeChange(); + + expect(event).toHaveBeenCalledWith('2021-07-26T07:20:00Z'); + }); }); diff --git a/frontend/src/app/components/ui-components/filter-fields/date/date.component.spec.ts b/frontend/src/app/components/ui-components/filter-fields/date/date.component.spec.ts index fc06c77f6..865159f87 100644 --- a/frontend/src/app/components/ui-components/filter-fields/date/date.component.spec.ts +++ b/frontend/src/app/components/ui-components/filter-fields/date/date.component.spec.ts @@ -1,46 +1,45 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DateFilterComponent } from './date.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { DateFilterComponent } from './date.component'; describe('DateFilterComponent', () => { - let component: DateFilterComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DateFilterComponent, BrowserAnimationsModule] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DateFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should prepare date for date input', () => { - component.value = '2021-06-26T07:22:00.603Z'; - component.ngOnInit(); - - expect(component.date).toEqual('2021-06-26'); - }); - - it('should remain date undefined if there is no value', () => { - component.value = null; - component.ngOnInit(); - - expect(component.date).not.toBeDefined(); - }); - - it('should send onChange event with new date value', () => { - component.date = '2021-07-26'; - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onDateChange(); - expect(event).toHaveBeenCalledWith('2021-07-26'); - }); + let component: DateFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DateFilterComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DateFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should prepare date for date input', () => { + component.value = '2021-06-26T07:22:00.603Z'; + component.ngOnInit(); + + expect(component.date).toEqual('2021-06-26'); + }); + + it('should remain date undefined if there is no value', () => { + component.value = null; + component.ngOnInit(); + + expect(component.date).not.toBeDefined(); + }); + + it('should send onChange event with new date value', () => { + component.date = '2021-07-26'; + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onDateChange(); + expect(event).toHaveBeenCalledWith('2021-07-26'); + }); }); diff --git a/frontend/src/app/components/ui-components/filter-fields/password/password.component.spec.ts b/frontend/src/app/components/ui-components/filter-fields/password/password.component.spec.ts index d94aece94..313eedcf6 100644 --- a/frontend/src/app/components/ui-components/filter-fields/password/password.component.spec.ts +++ b/frontend/src/app/components/ui-components/filter-fields/password/password.component.spec.ts @@ -1,32 +1,31 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { PasswordFilterComponent } from './password.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { PasswordFilterComponent } from './password.component'; describe('PasswordFilterComponent', () => { - let component: PasswordFilterComponent; - let fixture: ComponentFixture; + let component: PasswordFilterComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [PasswordFilterComponent, BrowserAnimationsModule] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PasswordFilterComponent, BrowserAnimationsModule], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(PasswordFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(PasswordFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should send onChange event with new null value if user clear password', () => { - component.clearPassword = true; - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onClearPasswordChange(); - expect(event).toHaveBeenCalledWith(null); - }); + it('should send onChange event with new null value if user clear password', () => { + component.clearPassword = true; + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onClearPasswordChange(); + expect(event).toHaveBeenCalledWith(null); + }); }); diff --git a/frontend/src/app/components/ui-components/filter-fields/timezone/timezone.component.spec.ts b/frontend/src/app/components/ui-components/filter-fields/timezone/timezone.component.spec.ts index a90f54e85..781af2fd1 100644 --- a/frontend/src/app/components/ui-components/filter-fields/timezone/timezone.component.spec.ts +++ b/frontend/src/app/components/ui-components/filter-fields/timezone/timezone.component.spec.ts @@ -4,49 +4,49 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TimezoneFilterComponent } from './timezone.component'; describe('TimezoneFilterComponent', () => { - let component: TimezoneFilterComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TimezoneFilterComponent, BrowserAnimationsModule] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(TimezoneFilterComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should populate timezones using Intl API', () => { - expect(component.timezones.length).toBeGreaterThan(0); - }); - - it('should include timezone offset in labels', () => { - const timezone = component.timezones.find(tz => tz.value === 'Europe/London'); - expect(timezone).toBeDefined(); - expect(timezone.label).toContain('UTC'); - }); - - it('should emit value on change', () => { - vi.spyOn(component.onFieldChange, 'emit'); - const testValue = 'Asia/Tokyo'; - component.value = testValue; - component.onFieldChange.emit(testValue); - expect(component.onFieldChange.emit).toHaveBeenCalledWith(testValue); - }); - - it('should add null option when allow_null is true', () => { - component.widgetStructure = { - widget_params: { allow_null: true } - } as any; - component.ngOnInit(); - const nullOption = component.timezones.find(tz => tz.value === null); - expect(nullOption).toBeDefined(); - }); + let component: TimezoneFilterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TimezoneFilterComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimezoneFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should populate timezones using Intl API', () => { + expect(component.timezones.length).toBeGreaterThan(0); + }); + + it('should include timezone offset in labels', () => { + const timezone = component.timezones.find((tz) => tz.value === 'Europe/London'); + expect(timezone).toBeDefined(); + expect(timezone.label).toContain('UTC'); + }); + + it('should emit value on change', () => { + vi.spyOn(component.onFieldChange, 'emit'); + const testValue = 'Asia/Tokyo'; + component.value = testValue; + component.onFieldChange.emit(testValue); + expect(component.onFieldChange.emit).toHaveBeenCalledWith(testValue); + }); + + it('should add null option when allow_null is true', () => { + component.widgetStructure = { + widget_params: { allow_null: true }, + } as any; + component.ngOnInit(); + const nullOption = component.timezones.find((tz) => tz.value === null); + expect(nullOption).toBeDefined(); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.spec.ts index 1463c74f0..b0f4a6440 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/date-time/date-time.component.spec.ts @@ -1,83 +1,77 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DBtype } from 'src/app/models/connection'; -import { DateTimeEditComponent } from './date-time.component'; import { MatDialogModule } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { provideHttpClient } from '@angular/common/http'; +import { DBtype } from 'src/app/models/connection'; +import { DateTimeEditComponent } from './date-time.component'; describe('DateTimeEditComponent', () => { - let component: DateTimeEditComponent; - let fixture: ComponentFixture; + let component: DateTimeEditComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - MatDialogModule, - DateTimeEditComponent, - BrowserAnimationsModule - ], - providers: [provideHttpClient()] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatSnackBarModule, MatDialogModule, DateTimeEditComponent, BrowserAnimationsModule], + providers: [provideHttpClient()], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(DateTimeEditComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(DateTimeEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should prepare date and time for date and time inputs', () => { - component.value = '2021-06-26T07:22:00.603'; - component.ngOnInit(); + it('should prepare date and time for date and time inputs', () => { + component.value = '2021-06-26T07:22:00.603'; + component.ngOnInit(); - expect(component.date).toEqual('2021-06-26'); - expect(component.time).toEqual('07:22:00'); - }); + expect(component.date).toEqual('2021-06-26'); + expect(component.time).toEqual('07:22:00'); + }); - it('should send onChange event with new date value if connectionType is not mysql', () => { - component.connectionType = DBtype.Postgres; - component.date = '2021-08-26'; - component.time = '07:22:00'; - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onDateChange(); + it('should send onChange event with new date value if connectionType is not mysql', () => { + component.connectionType = DBtype.Postgres; + component.date = '2021-08-26'; + component.time = '07:22:00'; + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onDateChange(); - expect(event).toHaveBeenCalledWith('2021-08-26T07:22:00Z'); - }); + expect(event).toHaveBeenCalledWith('2021-08-26T07:22:00Z'); + }); - it('should send onChange event with new date value if connectionType is mysql', () => { - component.connectionType = DBtype.MySQL; - component.date = '2021-08-26'; - component.time = '07:22:00'; - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onDateChange(); + it('should send onChange event with new date value if connectionType is mysql', () => { + component.connectionType = DBtype.MySQL; + component.date = '2021-08-26'; + component.time = '07:22:00'; + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onDateChange(); - expect(event).toHaveBeenCalledWith('2021-08-26 07:22:00'); - }); + expect(event).toHaveBeenCalledWith('2021-08-26 07:22:00'); + }); - it('should send onChange event with new time value if connectionType is not mysql', () => { - component.connectionType = DBtype.Postgres; - component.date = '2021-07-26'; - component.time = '07:20:00'; - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onTimeChange(); + it('should send onChange event with new time value if connectionType is not mysql', () => { + component.connectionType = DBtype.Postgres; + component.date = '2021-07-26'; + component.time = '07:20:00'; + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onTimeChange(); - expect(event).toHaveBeenCalledWith('2021-07-26T07:20:00Z'); - }); + expect(event).toHaveBeenCalledWith('2021-07-26T07:20:00Z'); + }); - it('should send onChange event with new time value if connectionType is mysql', () => { - component.connectionType = DBtype.MySQL; - component.date = '2021-07-26'; - component.time = '07:20:00'; - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onTimeChange(); + it('should send onChange event with new time value if connectionType is mysql', () => { + component.connectionType = DBtype.MySQL; + component.date = '2021-07-26'; + component.time = '07:20:00'; + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onTimeChange(); - expect(event).toHaveBeenCalledWith('2021-07-26 07:20:00'); - }); + expect(event).toHaveBeenCalledWith('2021-07-26 07:20:00'); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.spec.ts index 9e2c6a2dc..75865a1bf 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/date/date.component.spec.ts @@ -1,46 +1,45 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DateEditComponent } from './date.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { DateEditComponent } from './date.component'; describe('DateEditComponent', () => { - let component: DateEditComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DateEditComponent, BrowserAnimationsModule] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DateEditComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should prepare date for date input', () => { - component.value = '2021-06-26T07:22:00.603Z'; - component.ngOnInit(); - - expect(component.date).toEqual('2021-06-26'); - }); - - it('should remain date undefined if there is no value', () => { - component.value = null; - component.ngOnInit(); - - expect(component.date).not.toBeDefined(); - }); - - it('should send onChange event with new date value', () => { - component.date = '2021-07-26'; - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onDateChange(); - expect(event).toHaveBeenCalledWith('2021-07-26'); - }); + let component: DateEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DateEditComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DateEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should prepare date for date input', () => { + component.value = '2021-06-26T07:22:00.603Z'; + component.ngOnInit(); + + expect(component.date).toEqual('2021-06-26'); + }); + + it('should remain date undefined if there is no value', () => { + component.value = null; + component.ngOnInit(); + + expect(component.date).not.toBeDefined(); + }); + + it('should send onChange event with new date value', () => { + component.date = '2021-07-26'; + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onDateChange(); + expect(event).toHaveBeenCalledWith('2021-07-26'); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.ts index 4803862ad..343442eec 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/id/id.component.ts @@ -1,17 +1,16 @@ -import { Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; - -import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; +import { Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; @Component({ - selector: 'app-edit-id', - templateUrl: './id.component.html', - styleUrls: ['./id.component.css'], - imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule] + selector: 'app-edit-id', + templateUrl: './id.component.html', + styleUrls: ['./id.component.css'], + imports: [CommonModule, FormsModule, MatFormFieldModule, MatInputModule], }) export class IdEditComponent extends BaseEditFieldComponent { - @Input() value: string; + @Input() value: string; } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.spec.ts index 251fc8c47..9794d502e 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/money/money.component.spec.ts @@ -1,221 +1,221 @@ +import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { CommonModule } from '@angular/common'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { MoneyEditComponent } from './money.component'; describe('MoneyEditComponent', () => { - let component: MoneyEditComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - MoneyEditComponent, - CommonModule, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatSelectModule, - BrowserAnimationsModule - ] - }).compileComponents(); - - fixture = TestBed.createComponent(MoneyEditComponent); - component = fixture.componentInstance; - - // Set required properties from base component - component.label = 'Test Money'; - component.required = false; - component.disabled = false; - component.readonly = false; - - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should initialize with default currency USD and currency selector disabled', () => { - expect(component.selectedCurrency).toBe('USD'); - expect(component.defaultCurrency).toBe('USD'); - expect(component.showCurrencySelector).toBe(false); - }); - - it('should parse string value correctly', () => { - component.value = '100.50 EUR'; - component.ngOnInit(); - expect(component.selectedCurrency).toBe('EUR'); - expect(component.amount).toBe(100.5); - }); - - it('should parse object value correctly', () => { - component.value = { amount: 250.75, currency: 'GBP' }; - component.ngOnInit(); - expect(component.selectedCurrency).toBe('GBP'); - expect(component.amount).toBe(250.75); - }); - - it('should parse numeric value correctly when currency selector is disabled', () => { - component.value = 150.25; - component.ngOnInit(); - expect(component.selectedCurrency).toBe('USD'); - expect(component.amount).toBe(150.25); - expect(component.displayAmount).toBe('150.25'); - }); - - it('should handle empty value', () => { - component.value = ''; - component.ngOnInit(); - expect(component.selectedCurrency).toBe('USD'); - expect(component.amount).toBe(''); - }); - - it('should format amount with correct decimal places', () => { - component.decimalPlaces = 2; - const formatted = component.formatAmount(123.456); - expect(formatted).toBe('123.46'); - }); - - it('should handle currency change when selector is enabled', () => { - component.showCurrencySelector = true; - component.selectedCurrency = 'EUR'; - component.amount = 100; - vi.spyOn(component.onFieldChange, 'emit'); - - component.onCurrencyChange(); - - expect(component.onFieldChange.emit).toHaveBeenCalledWith({ - amount: 100, - currency: 'EUR' - }); - }); - - it('should handle amount change with currency selector disabled (default)', () => { - component.displayAmount = '123.45'; - component.selectedCurrency = 'USD'; - vi.spyOn(component.onFieldChange, 'emit'); - - component.onAmountChange(); - - expect(component.amount).toBe(123.45); - expect(component.onFieldChange.emit).toHaveBeenCalledWith(123.45); - }); - - it('should handle amount change with currency selector enabled', () => { - component.showCurrencySelector = true; - component.displayAmount = '123.45'; - component.selectedCurrency = 'USD'; - vi.spyOn(component.onFieldChange, 'emit'); - - component.onAmountChange(); - - expect(component.amount).toBe(123.45); - expect(component.onFieldChange.emit).toHaveBeenCalledWith({ - amount: 123.45, - currency: 'USD' - }); - }); - - it('should handle invalid amount input with letters', () => { - component.amount = 100; - component.displayAmount = 'abc123def'; // Contains letters which get stripped - - component.onAmountChange(); - component.onAmountBlur(); - - expect(component.amount).toBe(123); - expect(component.displayAmount).toBe('123.00'); - }); - - it('should handle completely invalid input', () => { - component.amount = 100 as string | number; - component.displayAmount = 'invalid'; // All letters, becomes empty after strip - - component.onAmountChange(); - - expect(component.amount as string).toBe(''); - expect(component.displayAmount).toBe(''); - }); - - it('should handle invalid amount input when amount is empty', () => { - component.amount = ''; - component.displayAmount = 'invalid'; - - component.onAmountChange(); - - expect(component.displayAmount).toBe(''); - }); - - it('should respect allow_negative configuration', () => { - component.allowNegative = false; - component.displayAmount = '-123.45'; - - component.onAmountChange(); - - expect(component.amount).toBe(123.45); - }); + let component: MoneyEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MoneyEditComponent, + CommonModule, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + BrowserAnimationsModule, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(MoneyEditComponent); + component = fixture.componentInstance; + + // Set required properties from base component + component.label = 'Test Money'; + component.required = false; + component.disabled = false; + component.readonly = false; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize with default currency USD and currency selector disabled', () => { + expect(component.selectedCurrency).toBe('USD'); + expect(component.defaultCurrency).toBe('USD'); + expect(component.showCurrencySelector).toBe(false); + }); + + it('should parse string value correctly', () => { + component.value = '100.50 EUR'; + component.ngOnInit(); + expect(component.selectedCurrency).toBe('EUR'); + expect(component.amount).toBe(100.5); + }); + + it('should parse object value correctly', () => { + component.value = { amount: 250.75, currency: 'GBP' }; + component.ngOnInit(); + expect(component.selectedCurrency).toBe('GBP'); + expect(component.amount).toBe(250.75); + }); + + it('should parse numeric value correctly when currency selector is disabled', () => { + component.value = 150.25; + component.ngOnInit(); + expect(component.selectedCurrency).toBe('USD'); + expect(component.amount).toBe(150.25); + expect(component.displayAmount).toBe('150.25'); + }); + + it('should handle empty value', () => { + component.value = ''; + component.ngOnInit(); + expect(component.selectedCurrency).toBe('USD'); + expect(component.amount).toBe(''); + }); + + it('should format amount with correct decimal places', () => { + component.decimalPlaces = 2; + const formatted = component.formatAmount(123.456); + expect(formatted).toBe('123.46'); + }); + + it('should handle currency change when selector is enabled', () => { + component.showCurrencySelector = true; + component.selectedCurrency = 'EUR'; + component.amount = 100; + vi.spyOn(component.onFieldChange, 'emit'); + + component.onCurrencyChange(); + + expect(component.onFieldChange.emit).toHaveBeenCalledWith({ + amount: 100, + currency: 'EUR', + }); + }); + + it('should handle amount change with currency selector disabled (default)', () => { + component.displayAmount = '123.45'; + component.selectedCurrency = 'USD'; + vi.spyOn(component.onFieldChange, 'emit'); + + component.onAmountChange(); + + expect(component.amount).toBe(123.45); + expect(component.onFieldChange.emit).toHaveBeenCalledWith(123.45); + }); + + it('should handle amount change with currency selector enabled', () => { + component.showCurrencySelector = true; + component.displayAmount = '123.45'; + component.selectedCurrency = 'USD'; + vi.spyOn(component.onFieldChange, 'emit'); + + component.onAmountChange(); + + expect(component.amount).toBe(123.45); + expect(component.onFieldChange.emit).toHaveBeenCalledWith({ + amount: 123.45, + currency: 'USD', + }); + }); + + it('should handle invalid amount input with letters', () => { + component.amount = 100; + component.displayAmount = 'abc123def'; // Contains letters which get stripped + + component.onAmountChange(); + component.onAmountBlur(); + + expect(component.amount).toBe(123); + expect(component.displayAmount).toBe('123.00'); + }); + + it('should handle completely invalid input', () => { + component.amount = 100 as string | number; + component.displayAmount = 'invalid'; // All letters, becomes empty after strip + + component.onAmountChange(); + + expect(component.amount as string).toBe(''); + expect(component.displayAmount).toBe(''); + }); + + it('should handle invalid amount input when amount is empty', () => { + component.amount = ''; + component.displayAmount = 'invalid'; + + component.onAmountChange(); + + expect(component.displayAmount).toBe(''); + }); + + it('should respect allow_negative configuration', () => { + component.allowNegative = false; + component.displayAmount = '-123.45'; + + component.onAmountChange(); + + expect(component.amount).toBe(123.45); + }); - it('should configure from widget params', () => { - component.widgetStructure = { - field_name: 'test_field', - widget_type: 'Money', - name: 'Test Widget', - description: 'Test Description', - widget_params: { - default_currency: 'EUR', - show_currency_selector: true, - decimal_places: 3, - allow_negative: false - } - }; + it('should configure from widget params', () => { + component.widgetStructure = { + field_name: 'test_field', + widget_type: 'Money', + name: 'Test Widget', + description: 'Test Description', + widget_params: { + default_currency: 'EUR', + show_currency_selector: true, + decimal_places: 3, + allow_negative: false, + }, + }; - component.configureFromWidgetParams(); + component.configureFromWidgetParams(); - expect(component.defaultCurrency).toBe('EUR'); - expect(component.showCurrencySelector).toBe(true); - expect(component.decimalPlaces).toBe(3); - expect(component.allowNegative).toBe(false); - }); + expect(component.defaultCurrency).toBe('EUR'); + expect(component.showCurrencySelector).toBe(true); + expect(component.decimalPlaces).toBe(3); + expect(component.allowNegative).toBe(false); + }); - it('should return correct display value', () => { - component.amount = 123.45; - component.selectedCurrency = 'USD'; + it('should return correct display value', () => { + component.amount = 123.45; + component.selectedCurrency = 'USD'; - const displayValue = component.displayValue; + const displayValue = component.displayValue; - expect(displayValue).toBe('$123.45'); - }); + expect(displayValue).toBe('$123.45'); + }); - it('should return correct placeholder', () => { - component.selectedCurrency = 'EUR'; + it('should return correct placeholder', () => { + component.selectedCurrency = 'EUR'; - const placeholder = component.placeholder; + const placeholder = component.placeholder; - expect(placeholder).toBe('Enter amount in Euro'); - }); + expect(placeholder).toBe('Enter amount in Euro'); + }); - it('should find selected currency data', () => { - component.selectedCurrency = 'GBP'; + it('should find selected currency data', () => { + component.selectedCurrency = 'GBP'; - const currencyData = component.selectedCurrencyData; + const currencyData = component.selectedCurrencyData; - expect(currencyData.code).toBe('GBP'); - expect(currencyData.name).toBe('British Pound'); - expect(currencyData.symbol).toBe('£'); - }); + expect(currencyData.code).toBe('GBP'); + expect(currencyData.name).toBe('British Pound'); + expect(currencyData.symbol).toBe('£'); + }); - it('should emit empty value when amount is cleared', () => { - component.amount = ''; - vi.spyOn(component.onFieldChange, 'emit'); + it('should emit empty value when amount is cleared', () => { + component.amount = ''; + vi.spyOn(component.onFieldChange, 'emit'); - component.updateValue(); + component.updateValue(); - expect(component.onFieldChange.emit).toHaveBeenCalledWith(''); - }); + expect(component.onFieldChange.emit).toHaveBeenCalledWith(''); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.spec.ts index 574a2e77b..91043c250 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/password/password.component.spec.ts @@ -1,97 +1,96 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { PasswordEditComponent } from './password.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { PasswordEditComponent } from './password.component'; describe('PasswordEditComponent', () => { - let component: PasswordEditComponent; - let fixture: ComponentFixture; + let component: PasswordEditComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [PasswordEditComponent, BrowserAnimationsModule] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PasswordEditComponent, BrowserAnimationsModule], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(PasswordEditComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(PasswordEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should send onChange event with new null value if user clear password', () => { - component.clearPassword = true; - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onClearPasswordChange(); - expect(event).toHaveBeenCalledWith(null); - }); + it('should send onChange event with new null value if user clear password', () => { + component.clearPassword = true; + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onClearPasswordChange(); + expect(event).toHaveBeenCalledWith(null); + }); - describe('ngOnInit', () => { - it('should reset masked password value to empty string', () => { - component.value = '***'; - component.ngOnInit(); - expect(component.value).toBe(''); - }); + describe('ngOnInit', () => { + it('should reset masked password value to empty string', () => { + component.value = '***'; + component.ngOnInit(); + expect(component.value).toBe(''); + }); - it('should not emit onFieldChange when password is masked (empty after reset)', () => { - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.value = '***'; - component.ngOnInit(); - expect(event).not.toHaveBeenCalled(); - }); + it('should not emit onFieldChange when password is masked (empty after reset)', () => { + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.value = '***'; + component.ngOnInit(); + expect(event).not.toHaveBeenCalled(); + }); - it('should emit onFieldChange when password has actual value', () => { - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.value = 'actualPassword'; - component.ngOnInit(); - expect(event).toHaveBeenCalledWith('actualPassword'); - }); + it('should emit onFieldChange when password has actual value', () => { + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.value = 'actualPassword'; + component.ngOnInit(); + expect(event).toHaveBeenCalledWith('actualPassword'); + }); - it('should not emit onFieldChange when password is empty string', () => { - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.value = ''; - component.ngOnInit(); - expect(event).not.toHaveBeenCalled(); - }); - }); + it('should not emit onFieldChange when password is empty string', () => { + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.value = ''; + component.ngOnInit(); + expect(event).not.toHaveBeenCalled(); + }); + }); - describe('onPasswordChange', () => { - it('should emit onFieldChange when password has value', () => { - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onPasswordChange('newPassword'); - expect(event).toHaveBeenCalledWith('newPassword'); - }); + describe('onPasswordChange', () => { + it('should emit onFieldChange when password has value', () => { + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onPasswordChange('newPassword'); + expect(event).toHaveBeenCalledWith('newPassword'); + }); - it('should not emit onFieldChange when password is empty string', () => { - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onPasswordChange(''); - expect(event).not.toHaveBeenCalled(); - }); + it('should not emit onFieldChange when password is empty string', () => { + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onPasswordChange(''); + expect(event).not.toHaveBeenCalled(); + }); - it('should emit onFieldChange when password is whitespace (actual value)', () => { - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.onPasswordChange(' '); - expect(event).toHaveBeenCalledWith(' '); - }); - }); + it('should emit onFieldChange when password is whitespace (actual value)', () => { + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.onPasswordChange(' '); + expect(event).toHaveBeenCalledWith(' '); + }); + }); - describe('onClearPasswordChange', () => { - it('should emit null when clearPassword is true', () => { - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.clearPassword = true; - component.onClearPasswordChange(); - expect(event).toHaveBeenCalledWith(null); - }); + describe('onClearPasswordChange', () => { + it('should emit null when clearPassword is true', () => { + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.clearPassword = true; + component.onClearPasswordChange(); + expect(event).toHaveBeenCalledWith(null); + }); - it('should not emit when clearPassword is false', () => { - const event = vi.spyOn(component.onFieldChange, 'emit'); - component.clearPassword = false; - component.onClearPasswordChange(); - expect(event).not.toHaveBeenCalled(); - }); - }); + it('should not emit when clearPassword is false', () => { + const event = vi.spyOn(component.onFieldChange, 'emit'); + component.clearPassword = false; + component.onClearPasswordChange(); + expect(event).not.toHaveBeenCalled(); + }); + }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts index 03d63d20f..87751b419 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/phone/phone.component.spec.ts @@ -1,314 +1,314 @@ +import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule, FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { CommonModule } from '@angular/common'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { PhoneEditComponent } from './phone.component'; describe('PhoneEditComponent', () => { - let component: PhoneEditComponent; - let fixture: ComponentFixture; + let component: PhoneEditComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - PhoneEditComponent, - CommonModule, - ReactiveFormsModule, - FormsModule, - MatFormFieldModule, - MatInputModule, - MatSelectModule, - MatAutocompleteModule, - NoopAnimationsModule - ] - }).compileComponents(); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + PhoneEditComponent, + CommonModule, + ReactiveFormsModule, + FormsModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatAutocompleteModule, + NoopAnimationsModule, + ], + }).compileComponents(); - fixture = TestBed.createComponent(PhoneEditComponent); - component = fixture.componentInstance; + fixture = TestBed.createComponent(PhoneEditComponent); + component = fixture.componentInstance; - // Set basic required properties - component.label = 'Phone'; - component.key = 'phone'; + // Set basic required properties + component.label = 'Phone'; + component.key = 'phone'; - fixture.detectChanges(); - }); + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - describe('US Phone Number Formatting', () => { - beforeEach(() => { - // Set US as selected country - const usCountry = component.countries.find(c => c.code === 'US'); - component.selectedCountry = usCountry!; - component.countryControl.setValue(usCountry!); - component.initializeFormatter(); - }); + describe('US Phone Number Formatting', () => { + beforeEach(() => { + // Set US as selected country + const usCountry = component.countries.find((c) => c.code === 'US'); + component.selectedCountry = usCountry!; + component.countryControl.setValue(usCountry!); + component.initializeFormatter(); + }); - it('should format US phone number in E164 format when user enters local number', () => { - const localNumber = '(202) 456-1111'; - component.displayPhoneNumber = localNumber; + it('should format US phone number in E164 format when user enters local number', () => { + const localNumber = '(202) 456-1111'; + component.displayPhoneNumber = localNumber; - component.onPhoneNumberChange(); + component.onPhoneNumberChange(); - expect(component.value).toBe('+12024561111'); - }); + expect(component.value).toBe('+12024561111'); + }); - it('should format US phone number in E164 format when user enters raw digits', () => { - const rawDigits = '2024561111'; - component.displayPhoneNumber = rawDigits; + it('should format US phone number in E164 format when user enters raw digits', () => { + const rawDigits = '2024561111'; + component.displayPhoneNumber = rawDigits; - component.onPhoneNumberChange(); + component.onPhoneNumberChange(); - expect(component.value).toBe('+12024561111'); - }); + expect(component.value).toBe('+12024561111'); + }); - it('should handle US phone number with different formatting', () => { - const formattedNumber = '202.456.1111'; - component.displayPhoneNumber = formattedNumber; + it('should handle US phone number with different formatting', () => { + const formattedNumber = '202.456.1111'; + component.displayPhoneNumber = formattedNumber; - component.onPhoneNumberChange(); + component.onPhoneNumberChange(); - expect(component.value).toBe('+12024561111'); - }); + expect(component.value).toBe('+12024561111'); + }); - it('should handle US phone number with country code already included', () => { - const withCountryCode = '+1 202 456 1111'; - component.displayPhoneNumber = withCountryCode; + it('should handle US phone number with country code already included', () => { + const withCountryCode = '+1 202 456 1111'; + component.displayPhoneNumber = withCountryCode; - component.onPhoneNumberChange(); + component.onPhoneNumberChange(); - expect(component.value).toBe('+12024561111'); - }); + expect(component.value).toBe('+12024561111'); + }); - it('should not format invalid US phone number', () => { - const invalidNumber = '123'; - component.displayPhoneNumber = invalidNumber; + it('should not format invalid US phone number', () => { + const invalidNumber = '123'; + component.displayPhoneNumber = invalidNumber; - component.onPhoneNumberChange(); + component.onPhoneNumberChange(); - // Should either be empty or the cleaned input, but not a malformed international number - expect(component.value).not.toMatch(/^\+1123$/); - }); - }); + // Should either be empty or the cleaned input, but not a malformed international number + expect(component.value).not.toMatch(/^\+1123$/); + }); + }); - describe('International Phone Number Formatting', () => { - it('should format UK phone number in E164 format', () => { - const ukCountry = component.countries.find(c => c.code === 'GB'); - component.selectedCountry = ukCountry!; - component.countryControl.setValue(ukCountry!); - component.initializeFormatter(); + describe('International Phone Number Formatting', () => { + it('should format UK phone number in E164 format', () => { + const ukCountry = component.countries.find((c) => c.code === 'GB'); + component.selectedCountry = ukCountry!; + component.countryControl.setValue(ukCountry!); + component.initializeFormatter(); - const localNumber = '020 7946 0958'; - component.displayPhoneNumber = localNumber; + const localNumber = '020 7946 0958'; + component.displayPhoneNumber = localNumber; - component.onPhoneNumberChange(); + component.onPhoneNumberChange(); - expect(component.value).toBe('+442079460958'); - }); + expect(component.value).toBe('+442079460958'); + }); - it('should format German phone number in E164 format', () => { - const deCountry = component.countries.find(c => c.code === 'DE'); - component.selectedCountry = deCountry!; - component.countryControl.setValue(deCountry!); - component.initializeFormatter(); + it('should format German phone number in E164 format', () => { + const deCountry = component.countries.find((c) => c.code === 'DE'); + component.selectedCountry = deCountry!; + component.countryControl.setValue(deCountry!); + component.initializeFormatter(); - const localNumber = '030 12345678'; - component.displayPhoneNumber = localNumber; + const localNumber = '030 12345678'; + component.displayPhoneNumber = localNumber; - component.onPhoneNumberChange(); + component.onPhoneNumberChange(); - expect(component.value).toBe('+493012345678'); - }); - }); + expect(component.value).toBe('+493012345678'); + }); + }); - describe('Phone Number Validation', () => { - beforeEach(() => { - component.phoneValidation = true; - }); + describe('Phone Number Validation', () => { + beforeEach(() => { + component.phoneValidation = true; + }); - it('should validate US phone number as valid', () => { - const usCountry = component.countries.find(c => c.code === 'US'); - component.selectedCountry = usCountry!; - component.displayPhoneNumber = '(202) 456-1111'; + it('should validate US phone number as valid', () => { + const usCountry = component.countries.find((c) => c.code === 'US'); + component.selectedCountry = usCountry!; + component.displayPhoneNumber = '(202) 456-1111'; - const isValid = component.isValidPhoneNumber(); + const isValid = component.isValidPhoneNumber(); - expect(isValid).toBe(true); - }); + expect(isValid).toBe(true); + }); - it('should validate international phone number as valid', () => { - component.displayPhoneNumber = '+442079460958'; + it('should validate international phone number as valid', () => { + component.displayPhoneNumber = '+442079460958'; - const isValid = component.isValidPhoneNumber(); + const isValid = component.isValidPhoneNumber(); - expect(isValid).toBe(true); - }); + expect(isValid).toBe(true); + }); - it('should validate invalid phone number as invalid', () => { - const usCountry = component.countries.find(c => c.code === 'US'); - component.selectedCountry = usCountry!; - component.displayPhoneNumber = '123'; + it('should validate invalid phone number as invalid', () => { + const usCountry = component.countries.find((c) => c.code === 'US'); + component.selectedCountry = usCountry!; + component.displayPhoneNumber = '123'; - const isValid = component.isValidPhoneNumber(); + const isValid = component.isValidPhoneNumber(); - expect(isValid).toBe(false); - }); + expect(isValid).toBe(false); + }); - it('should treat empty phone number as valid when validation is enabled', () => { - component.displayPhoneNumber = ''; + it('should treat empty phone number as valid when validation is enabled', () => { + component.displayPhoneNumber = ''; - const isValid = component.isValidPhoneNumber(); + const isValid = component.isValidPhoneNumber(); - expect(isValid).toBe(true); // Empty is valid, let required validation handle it - }); - }); + expect(isValid).toBe(true); // Empty is valid, let required validation handle it + }); + }); - describe('Country Detection', () => { - it('should detect country from international number', () => { - const internationalNumber = '+442079460958'; - component.displayPhoneNumber = internationalNumber; + describe('Country Detection', () => { + it('should detect country from international number', () => { + const internationalNumber = '+442079460958'; + component.displayPhoneNumber = internationalNumber; - component.onPhoneNumberChange(); + component.onPhoneNumberChange(); - expect(component.selectedCountry.code).toBe('GB'); - expect(component.countryControl.value?.code).toBe('GB'); - }); + expect(component.selectedCountry.code).toBe('GB'); + expect(component.countryControl.value?.code).toBe('GB'); + }); - it('should detect US from +1 number', () => { - const usInternationalNumber = '+12024561111'; - component.displayPhoneNumber = usInternationalNumber; + it('should detect US from +1 number', () => { + const usInternationalNumber = '+12024561111'; + component.displayPhoneNumber = usInternationalNumber; - component.onPhoneNumberChange(); + component.onPhoneNumberChange(); - expect(component.selectedCountry.code).toBe('US'); - expect(component.countryControl.value?.code).toBe('US'); - }); - }); + expect(component.selectedCountry.code).toBe('US'); + expect(component.countryControl.value?.code).toBe('US'); + }); + }); - describe('Autocomplete Functionality', () => { - it('should filter countries by name', () => { - const filtered = component._filterCountries('United'); + describe('Autocomplete Functionality', () => { + it('should filter countries by name', () => { + const filtered = component._filterCountries('United'); - expect(filtered.length).toBeGreaterThan(0); - expect(filtered.some(country => country.name.includes('United'))).toBe(true); - }); + expect(filtered.length).toBeGreaterThan(0); + expect(filtered.some((country) => country.name.includes('United'))).toBe(true); + }); - it('should filter countries by code', () => { - const filtered = component._filterCountries('US'); + it('should filter countries by code', () => { + const filtered = component._filterCountries('US'); - expect(filtered.length).toBeGreaterThan(0); - expect(filtered.some(country => country.code === 'US')).toBe(true); - }); + expect(filtered.length).toBeGreaterThan(0); + expect(filtered.some((country) => country.code === 'US')).toBe(true); + }); - it('should filter countries by dial code', () => { - const filtered = component._filterCountries('+1'); + it('should filter countries by dial code', () => { + const filtered = component._filterCountries('+1'); - expect(filtered.length).toBeGreaterThan(0); - expect(filtered.some(country => country.dialCode === '+1')).toBe(true); - }); + expect(filtered.length).toBeGreaterThan(0); + expect(filtered.some((country) => country.dialCode === '+1')).toBe(true); + }); - it('should display country with flag, name and dial code', () => { - const usCountry = component.countries.find(c => c.code === 'US')!; + it('should display country with flag, name and dial code', () => { + const usCountry = component.countries.find((c) => c.code === 'US')!; - const displayText = component.displayCountryFn(usCountry); + const displayText = component.displayCountryFn(usCountry); - expect(displayText).toContain('🇺🇸'); - expect(displayText).toContain('United States'); - expect(displayText).toContain('+1'); - }); + expect(displayText).toContain('🇺🇸'); + expect(displayText).toContain('United States'); + expect(displayText).toContain('+1'); + }); - it('should handle country selection', () => { - const ukCountry = component.countries.find(c => c.code === 'GB')!; + it('should handle country selection', () => { + const ukCountry = component.countries.find((c) => c.code === 'GB')!; - component.onCountrySelected(ukCountry); + component.onCountrySelected(ukCountry); - expect(component.selectedCountry).toBe(ukCountry); - expect(component.formatter).toBeDefined(); - }); - }); + expect(component.selectedCountry).toBe(ukCountry); + expect(component.formatter).toBeDefined(); + }); + }); - describe('Example Phone Numbers', () => { - it('should return correct example for US', () => { - const usCountry = component.countries.find(c => c.code === 'US')!; - component.selectedCountry = usCountry; + describe('Example Phone Numbers', () => { + it('should return correct example for US', () => { + const usCountry = component.countries.find((c) => c.code === 'US')!; + component.selectedCountry = usCountry; - const example = component.getExamplePhoneNumber(); + const example = component.getExamplePhoneNumber(); - expect(example).toBe('(202) 456-1111'); - }); + expect(example).toBe('(202) 456-1111'); + }); - it('should return correct example for UK', () => { - const ukCountry = component.countries.find(c => c.code === 'GB')!; - component.selectedCountry = ukCountry; + it('should return correct example for UK', () => { + const ukCountry = component.countries.find((c) => c.code === 'GB')!; + component.selectedCountry = ukCountry; - const example = component.getExamplePhoneNumber(); + const example = component.getExamplePhoneNumber(); - expect(example).toBe('020 7946 0958'); - }); + expect(example).toBe('020 7946 0958'); + }); - it('should return fallback example for unknown country', () => { - component.selectedCountry = { code: 'XX', name: 'Unknown', dialCode: '+999', flag: '🏳️' }; + it('should return fallback example for unknown country', () => { + component.selectedCountry = { code: 'XX', name: 'Unknown', dialCode: '+999', flag: '🏳️' }; - const example = component.getExamplePhoneNumber(); + const example = component.getExamplePhoneNumber(); - expect(example).toBe('+999 123 4567'); - }); - }); + expect(example).toBe('+999 123 4567'); + }); + }); - describe('Edge Cases and Error Handling', () => { - it('should handle malformed phone numbers gracefully', () => { - component.displayPhoneNumber = 'not-a-phone-number'; + describe('Edge Cases and Error Handling', () => { + it('should handle malformed phone numbers gracefully', () => { + component.displayPhoneNumber = 'not-a-phone-number'; - expect(() => component.onPhoneNumberChange()).not.toThrow(); - }); + expect(() => component.onPhoneNumberChange()).not.toThrow(); + }); - it('should handle empty selected country', () => { - component.selectedCountry = null as any; - component.displayPhoneNumber = '5551234567'; + it('should handle empty selected country', () => { + component.selectedCountry = null as any; + component.displayPhoneNumber = '5551234567'; - expect(() => component.onPhoneNumberChange()).not.toThrow(); - }); + expect(() => component.onPhoneNumberChange()).not.toThrow(); + }); - it('should emit field change events', () => { - vi.spyOn(component.onFieldChange, 'emit'); + it('should emit field change events', () => { + vi.spyOn(component.onFieldChange, 'emit'); - const usCountry = component.countries.find(c => c.code === 'US'); - component.selectedCountry = usCountry!; - component.displayPhoneNumber = '5551234567'; + const usCountry = component.countries.find((c) => c.code === 'US'); + component.selectedCountry = usCountry!; + component.displayPhoneNumber = '5551234567'; - component.onPhoneNumberChange(); + component.onPhoneNumberChange(); - expect(component.onFieldChange.emit).toHaveBeenCalledWith(component.value); - }); - }); + expect(component.onFieldChange.emit).toHaveBeenCalledWith(component.value); + }); + }); - describe('Widget Configuration', () => { - it('should configure from widget params', () => { - component.widgetStructure = { - widget_params: { - preferred_countries: ['CA', 'GB'], - enable_placeholder: false, - phone_validation: false - } - } as any; + describe('Widget Configuration', () => { + it('should configure from widget params', () => { + component.widgetStructure = { + widget_params: { + preferred_countries: ['CA', 'GB'], + enable_placeholder: false, + phone_validation: false, + }, + } as any; - component.configureFromWidgetParams(); + component.configureFromWidgetParams(); - expect(component.preferredCountries).toEqual(['CA', 'GB']); - expect(component.enablePlaceholder).toBe(false); - expect(component.phoneValidation).toBe(false); - }); + expect(component.preferredCountries).toEqual(['CA', 'GB']); + expect(component.enablePlaceholder).toBe(false); + expect(component.phoneValidation).toBe(false); + }); - it('should handle missing widget params', () => { - component.widgetStructure = {} as any; + it('should handle missing widget params', () => { + component.widgetStructure = {} as any; - expect(() => component.configureFromWidgetParams()).not.toThrow(); - }); - }); -}); \ No newline at end of file + expect(() => component.configureFromWidgetParams()).not.toThrow(); + }); + }); +}); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.ts index 61e4087ef..99f57ee00 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/point/point.component.ts @@ -1,17 +1,16 @@ -import { Component, Input } from '@angular/core'; import { CommonModule } from '@angular/common'; - -import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; +import { Component, Input } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; @Component({ - selector: 'app-edit-point', - templateUrl: './point.component.html', - styleUrls: ['./point.component.css'], - imports: [CommonModule, MatFormFieldModule, MatInputModule, FormsModule] + selector: 'app-edit-point', + templateUrl: './point.component.html', + styleUrls: ['./point.component.css'], + imports: [CommonModule, MatFormFieldModule, MatInputModule, FormsModule], }) export class PointEditComponent extends BaseEditFieldComponent { - @Input() value; + @Input() value; } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.spec.ts index 0e1195efb..21d02474e 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/s3/s3.component.spec.ts @@ -1,17 +1,14 @@ -import { - ComponentFixture, - TestBed, -} from "@angular/core/testing"; -import { FormsModule } from "@angular/forms"; -import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; -import { of, Subject, throwError } from "rxjs"; -import { WidgetStructure } from "src/app/models/table"; -import { ConnectionsService } from "src/app/services/connections.service"; -import { S3Service } from "src/app/services/s3.service"; -import { TablesService } from "src/app/services/tables.service"; -import { S3EditComponent } from "./s3.component"; - -describe("S3EditComponent", () => { +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { of, Subject, throwError } from 'rxjs'; +import { WidgetStructure } from 'src/app/models/table'; +import { ConnectionsService } from 'src/app/services/connections.service'; +import { S3Service } from 'src/app/services/s3.service'; +import { TablesService } from 'src/app/services/tables.service'; +import { S3EditComponent } from './s3.component'; + +describe('S3EditComponent', () => { let component: S3EditComponent; let fixture: ComponentFixture; let fakeS3Service: any; @@ -19,43 +16,42 @@ describe("S3EditComponent", () => { let fakeTablesService: any; const mockWidgetStructure: WidgetStructure = { - field_name: "document", - widget_type: "S3", + field_name: 'document', + widget_type: 'S3', widget_params: { - bucket: "test-bucket", - prefix: "uploads/", - region: "us-east-1", - aws_access_key_id_secret_name: "aws-key", - aws_secret_access_key_secret_name: "aws-secret", + bucket: 'test-bucket', + prefix: 'uploads/', + region: 'us-east-1', + aws_access_key_id_secret_name: 'aws-key', + aws_secret_access_key_secret_name: 'aws-secret', }, - name: "Document Upload", - description: "Upload documents to S3", + name: 'Document Upload', + description: 'Upload documents to S3', }; const mockWidgetStructureStringParams: WidgetStructure = { - field_name: "document", - widget_type: "S3", + field_name: 'document', + widget_type: 'S3', widget_params: JSON.stringify({ - bucket: "test-bucket", - prefix: "uploads/", - region: "us-east-1", - aws_access_key_id_secret_name: "aws-key", - aws_secret_access_key_secret_name: "aws-secret", + bucket: 'test-bucket', + prefix: 'uploads/', + region: 'us-east-1', + aws_access_key_id_secret_name: 'aws-key', + aws_secret_access_key_secret_name: 'aws-secret', }) as any, - name: "Document Upload", - description: "Upload documents to S3", + name: 'Document Upload', + description: 'Upload documents to S3', }; const mockFileUrlResponse = { - url: "https://s3.amazonaws.com/bucket/file.pdf?signature=abc123", - key: "uploads/file.pdf", + url: 'https://s3.amazonaws.com/bucket/file.pdf?signature=abc123', + key: 'uploads/file.pdf', expiresIn: 3600, }; const mockUploadUrlResponse = { - uploadUrl: - "https://s3.amazonaws.com/bucket/uploads/newfile.pdf?signature=xyz789", - key: "uploads/newfile.pdf", + uploadUrl: 'https://s3.amazonaws.com/bucket/uploads/newfile.pdf?signature=xyz789', + key: 'uploads/newfile.pdf', expiresIn: 3600, }; @@ -66,10 +62,14 @@ describe("S3EditComponent", () => { uploadToS3: vi.fn(), } as any; fakeConnectionsService = { - get currentConnectionID() { return "conn-123"; } + get currentConnectionID() { + return 'conn-123'; + }, } as any; fakeTablesService = { - get currentTableName() { return "users"; } + get currentTableName() { + return 'users'; + }, } as any; await TestBed.configureTestingModule({ @@ -86,92 +86,87 @@ describe("S3EditComponent", () => { fixture = TestBed.createComponent(S3EditComponent); component = fixture.componentInstance; - component.key = "document"; - component.label = "Document"; + component.key = 'document'; + component.label = 'Document'; component.widgetStructure = mockWidgetStructure; component.rowPrimaryKey = { id: 1 }; }); - it("should create", () => { + it('should create', () => { fixture.detectChanges(); expect(component).toBeTruthy(); }); - describe("ngOnInit", () => { - it("should set connectionId and tableName from services", () => { + describe('ngOnInit', () => { + it('should set connectionId and tableName from services', () => { fixture.detectChanges(); - expect((component as any).connectionId).toBe("conn-123"); - expect((component as any).tableName).toBe("users"); + expect((component as any).connectionId).toBe('conn-123'); + expect((component as any).tableName).toBe('users'); }); - it("should parse widget params from object", () => { + it('should parse widget params from object', () => { component.widgetStructure = mockWidgetStructure; fixture.detectChanges(); expect(component.params).toEqual({ - bucket: "test-bucket", - prefix: "uploads/", - region: "us-east-1", - aws_access_key_id_secret_name: "aws-key", - aws_secret_access_key_secret_name: "aws-secret", + bucket: 'test-bucket', + prefix: 'uploads/', + region: 'us-east-1', + aws_access_key_id_secret_name: 'aws-key', + aws_secret_access_key_secret_name: 'aws-secret', }); }); - it("should parse widget params from string", () => { + it('should parse widget params from string', () => { component.widgetStructure = mockWidgetStructureStringParams; fixture.detectChanges(); - expect(component.params.bucket).toBe("test-bucket"); + expect(component.params.bucket).toBe('test-bucket'); }); - it("should load preview if value is present", () => { - component.value = "uploads/existing-file.pdf"; + it('should load preview if value is present', () => { + component.value = 'uploads/existing-file.pdf'; fakeS3Service.getFileUrl.mockReturnValue(of(mockFileUrlResponse)); fixture.detectChanges(); - expect(fakeS3Service.getFileUrl).toHaveBeenCalledWith( - "conn-123", - "users", - "document", - { id: 1 }, - ); + expect(fakeS3Service.getFileUrl).toHaveBeenCalledWith('conn-123', 'users', 'document', { id: 1 }); }); - it("should not load preview if value is empty", () => { - component.value = ""; + it('should not load preview if value is empty', () => { + component.value = ''; fixture.detectChanges(); expect(fakeS3Service.getFileUrl).not.toHaveBeenCalled(); }); }); - describe("ngOnChanges", () => { - it("should load preview when value changes and no preview exists", () => { + describe('ngOnChanges', () => { + it('should load preview when value changes and no preview exists', () => { fixture.detectChanges(); fakeS3Service.getFileUrl.mockReturnValue(of(mockFileUrlResponse)); - component.value = "uploads/new-file.pdf"; + component.value = 'uploads/new-file.pdf'; component.ngOnChanges(); expect(fakeS3Service.getFileUrl).toHaveBeenCalled(); }); - it("should not reload preview if already loading", () => { + it('should not reload preview if already loading', () => { fixture.detectChanges(); component.isLoading = true; - component.value = "uploads/file.pdf"; + component.value = 'uploads/file.pdf'; component.ngOnChanges(); expect(fakeS3Service.getFileUrl).not.toHaveBeenCalled(); }); - it("should not reload preview if preview already exists", () => { + it('should not reload preview if preview already exists', () => { fixture.detectChanges(); - component.previewUrl = "https://example.com/preview"; - component.value = "uploads/file.pdf"; + component.previewUrl = 'https://example.com/preview'; + component.value = 'uploads/file.pdf'; component.ngOnChanges(); @@ -179,15 +174,15 @@ describe("S3EditComponent", () => { }); }); - describe("onFileSelected", () => { - it("should upload file and update value on success", async () => { + describe('onFileSelected', () => { + it('should upload file and update value on success', async () => { fixture.detectChanges(); fakeS3Service.getUploadUrl.mockReturnValue(of(mockUploadUrlResponse)); fakeS3Service.uploadToS3.mockReturnValue(of(undefined)); fakeS3Service.getFileUrl.mockReturnValue(of(mockFileUrlResponse)); - const file = new File(["test content"], "test.pdf", { - type: "application/pdf", + const file = new File(['test content'], 'test.pdf', { + type: 'application/pdf', }); const event = { target: { @@ -195,28 +190,23 @@ describe("S3EditComponent", () => { }, } as unknown as Event; - vi.spyOn(component.onFieldChange, "emit"); + vi.spyOn(component.onFieldChange, 'emit'); component.onFileSelected(event); await fixture.whenStable(); expect(fakeS3Service.getUploadUrl).toHaveBeenCalledWith( - "conn-123", - "users", - "document", - "test.pdf", - "application/pdf", - ); - expect(fakeS3Service.uploadToS3).toHaveBeenCalledWith( - mockUploadUrlResponse.uploadUrl, - file, - ); - expect(component.value).toBe("uploads/newfile.pdf"); - expect(component.onFieldChange.emit).toHaveBeenCalledWith( - "uploads/newfile.pdf", + 'conn-123', + 'users', + 'document', + 'test.pdf', + 'application/pdf', ); + expect(fakeS3Service.uploadToS3).toHaveBeenCalledWith(mockUploadUrlResponse.uploadUrl, file); + expect(component.value).toBe('uploads/newfile.pdf'); + expect(component.onFieldChange.emit).toHaveBeenCalledWith('uploads/newfile.pdf'); }); - it("should do nothing if no files selected", () => { + it('should do nothing if no files selected', () => { fixture.detectChanges(); const event = { target: { @@ -229,7 +219,7 @@ describe("S3EditComponent", () => { expect(fakeS3Service.getUploadUrl).not.toHaveBeenCalled(); }); - it("should do nothing if files is null", () => { + it('should do nothing if files is null', () => { fixture.detectChanges(); const event = { target: { @@ -242,14 +232,14 @@ describe("S3EditComponent", () => { expect(fakeS3Service.getUploadUrl).not.toHaveBeenCalled(); }); - it("should set isLoading to true during upload", () => { + it('should set isLoading to true during upload', () => { fixture.detectChanges(); fakeS3Service.getUploadUrl.mockReturnValue(of(mockUploadUrlResponse)); // Use a Subject that never emits to keep the upload "in progress" const pendingUpload$ = new Subject(); fakeS3Service.uploadToS3.mockReturnValue(pendingUpload$.asObservable()); - const file = new File(["test"], "test.pdf", { type: "application/pdf" }); + const file = new File(['test'], 'test.pdf', { type: 'application/pdf' }); const event = { target: { files: [file] } } as unknown as Event; component.onFileSelected(event); @@ -257,13 +247,11 @@ describe("S3EditComponent", () => { expect(component.isLoading).toBe(true); }); - it("should set isLoading to false on getUploadUrl error", async () => { + it('should set isLoading to false on getUploadUrl error', async () => { fixture.detectChanges(); - fakeS3Service.getUploadUrl.mockReturnValue( - throwError(() => new Error("Upload URL error")), - ); + fakeS3Service.getUploadUrl.mockReturnValue(throwError(() => new Error('Upload URL error'))); - const file = new File(["test"], "test.pdf", { type: "application/pdf" }); + const file = new File(['test'], 'test.pdf', { type: 'application/pdf' }); const event = { target: { files: [file] } } as unknown as Event; component.onFileSelected(event); @@ -272,14 +260,12 @@ describe("S3EditComponent", () => { expect(component.isLoading).toBe(false); }); - it("should set isLoading to false on uploadToS3 error", async () => { + it('should set isLoading to false on uploadToS3 error', async () => { fixture.detectChanges(); fakeS3Service.getUploadUrl.mockReturnValue(of(mockUploadUrlResponse)); - fakeS3Service.uploadToS3.mockReturnValue( - throwError(() => new Error("S3 upload error")), - ); + fakeS3Service.uploadToS3.mockReturnValue(throwError(() => new Error('S3 upload error'))); - const file = new File(["test"], "test.pdf", { type: "application/pdf" }); + const file = new File(['test'], 'test.pdf', { type: 'application/pdf' }); const event = { target: { files: [file] } } as unknown as Event; component.onFileSelected(event); @@ -289,24 +275,21 @@ describe("S3EditComponent", () => { }); }); - describe("openFile", () => { - it("should open preview URL in new tab", () => { + describe('openFile', () => { + it('should open preview URL in new tab', () => { fixture.detectChanges(); - component.previewUrl = "https://s3.amazonaws.com/bucket/file.pdf"; - vi.spyOn(window, "open"); + component.previewUrl = 'https://s3.amazonaws.com/bucket/file.pdf'; + vi.spyOn(window, 'open'); component.openFile(); - expect(window.open).toHaveBeenCalledWith( - "https://s3.amazonaws.com/bucket/file.pdf", - "_blank", - ); + expect(window.open).toHaveBeenCalledWith('https://s3.amazonaws.com/bucket/file.pdf', '_blank'); }); - it("should not open if previewUrl is null", () => { + it('should not open if previewUrl is null', () => { fixture.detectChanges(); component.previewUrl = null; - vi.spyOn(window, "open"); + vi.spyOn(window, 'open'); component.openFile(); @@ -314,22 +297,22 @@ describe("S3EditComponent", () => { }); }); - describe("_isImageFile", () => { + describe('_isImageFile', () => { const testCases = [ - { key: "photo.jpg", expected: true }, - { key: "photo.JPG", expected: true }, - { key: "photo.jpeg", expected: true }, - { key: "photo.png", expected: true }, - { key: "photo.gif", expected: true }, - { key: "photo.webp", expected: true }, - { key: "photo.svg", expected: true }, - { key: "photo.bmp", expected: true }, - { key: "document.pdf", expected: false }, - { key: "document.doc", expected: false }, - { key: "data.csv", expected: false }, - { key: "archive.zip", expected: false }, - { key: "uploads/folder/photo.png", expected: true }, - { key: "file-without-extension", expected: false }, + { key: 'photo.jpg', expected: true }, + { key: 'photo.JPG', expected: true }, + { key: 'photo.jpeg', expected: true }, + { key: 'photo.png', expected: true }, + { key: 'photo.gif', expected: true }, + { key: 'photo.webp', expected: true }, + { key: 'photo.svg', expected: true }, + { key: 'photo.bmp', expected: true }, + { key: 'document.pdf', expected: false }, + { key: 'document.doc', expected: false }, + { key: 'data.csv', expected: false }, + { key: 'archive.zip', expected: false }, + { key: 'uploads/folder/photo.png', expected: true }, + { key: 'file-without-extension', expected: false }, ]; testCases.forEach(({ key, expected }) => { @@ -341,9 +324,9 @@ describe("S3EditComponent", () => { }); }); - describe("_loadPreview", () => { - it("should set previewUrl and isImage on successful load", async () => { - component.value = "uploads/photo.jpg"; + describe('_loadPreview', () => { + it('should set previewUrl and isImage on successful load', async () => { + component.value = 'uploads/photo.jpg'; fakeS3Service.getFileUrl.mockReturnValue(of(mockFileUrlResponse)); fixture.detectChanges(); @@ -354,8 +337,8 @@ describe("S3EditComponent", () => { expect(component.isLoading).toBe(false); }); - it("should set isImage to false for non-image files", async () => { - component.value = "uploads/document.pdf"; + it('should set isImage to false for non-image files', async () => { + component.value = 'uploads/document.pdf'; fakeS3Service.getFileUrl.mockReturnValue(of(mockFileUrlResponse)); fixture.detectChanges(); @@ -364,25 +347,25 @@ describe("S3EditComponent", () => { expect(component.isImage).toBe(false); }); - it("should not load preview if value is empty", () => { - component.value = ""; + it('should not load preview if value is empty', () => { + component.value = ''; fixture.detectChanges(); expect(fakeS3Service.getFileUrl).not.toHaveBeenCalled(); }); - it("should not load preview if connectionId is missing", () => { - (component as any).connectionId = ""; - component.value = "uploads/file.pdf"; + it('should not load preview if connectionId is missing', () => { + (component as any).connectionId = ''; + component.value = 'uploads/file.pdf'; (component as any)._loadPreview(); expect(fakeS3Service.getFileUrl).not.toHaveBeenCalled(); }); - it("should not load preview if tableName is missing", () => { + it('should not load preview if tableName is missing', () => { fixture.detectChanges(); - (component as any).tableName = ""; - component.value = "uploads/file.pdf"; + (component as any).tableName = ''; + component.value = 'uploads/file.pdf'; fakeS3Service.getFileUrl.mockClear(); (component as any)._loadPreview(); @@ -390,19 +373,17 @@ describe("S3EditComponent", () => { expect(fakeS3Service.getFileUrl).not.toHaveBeenCalled(); }); - it("should not load preview if rowPrimaryKey is missing", () => { + it('should not load preview if rowPrimaryKey is missing', () => { component.rowPrimaryKey = null as any; - component.value = "uploads/file.pdf"; + component.value = 'uploads/file.pdf'; fixture.detectChanges(); expect(fakeS3Service.getFileUrl).not.toHaveBeenCalled(); }); - it("should set isLoading to false on error", async () => { - component.value = "uploads/file.pdf"; - fakeS3Service.getFileUrl.mockReturnValue( - throwError(() => new Error("File URL error")), - ); + it('should set isLoading to false on error', async () => { + component.value = 'uploads/file.pdf'; + fakeS3Service.getFileUrl.mockReturnValue(throwError(() => new Error('File URL error'))); fixture.detectChanges(); await fixture.whenStable(); @@ -411,15 +392,15 @@ describe("S3EditComponent", () => { }); }); - describe("_parseWidgetParams", () => { - it("should handle undefined widgetStructure gracefully", () => { + describe('_parseWidgetParams', () => { + it('should handle undefined widgetStructure gracefully', () => { component.widgetStructure = undefined as any; fixture.detectChanges(); expect(component.params).toBeUndefined(); }); - it("should handle null widget_params gracefully", () => { + it('should handle null widget_params gracefully', () => { component.widgetStructure = { ...mockWidgetStructure, widget_params: null as any, @@ -429,11 +410,11 @@ describe("S3EditComponent", () => { expect(component.params).toBeUndefined(); }); - it("should handle invalid JSON string gracefully", () => { - vi.spyOn(console, "error"); + it('should handle invalid JSON string gracefully', () => { + vi.spyOn(console, 'error'); component.widgetStructure = { ...mockWidgetStructure, - widget_params: "invalid json" as any, + widget_params: 'invalid json' as any, }; fixture.detectChanges(); @@ -441,112 +422,103 @@ describe("S3EditComponent", () => { }); }); - describe("template integration", () => { + describe('template integration', () => { beforeEach(() => { fixture.detectChanges(); }); - it("should display label in form field", () => { - const label = fixture.nativeElement.querySelector("mat-label"); - expect(label.textContent).toContain("Document"); + it('should display label in form field', () => { + const label = fixture.nativeElement.querySelector('mat-label'); + expect(label.textContent).toContain('Document'); }); - it("should show upload button", () => { - const uploadButton = fixture.nativeElement.querySelector("button"); - expect(uploadButton.textContent).toContain("Upload"); + it('should show upload button', () => { + const uploadButton = fixture.nativeElement.querySelector('button'); + expect(uploadButton.textContent).toContain('Upload'); }); - it("should disable upload button when disabled", () => { + it('should disable upload button when disabled', () => { component.disabled = true; fixture.detectChanges(); - const uploadButton = fixture.nativeElement.querySelector("button"); + const uploadButton = fixture.nativeElement.querySelector('button'); expect(uploadButton.disabled).toBe(true); }); - it("should disable upload button when readonly", () => { + it('should disable upload button when readonly', () => { component.readonly = true; fixture.detectChanges(); - const uploadButton = fixture.nativeElement.querySelector("button"); + const uploadButton = fixture.nativeElement.querySelector('button'); expect(uploadButton.disabled).toBe(true); }); - it("should disable upload button when loading", () => { + it('should disable upload button when loading', () => { component.isLoading = true; fixture.detectChanges(); - const uploadButton = fixture.nativeElement.querySelector("button"); + const uploadButton = fixture.nativeElement.querySelector('button'); expect(uploadButton.disabled).toBe(true); }); - it("should show open button when previewUrl exists", () => { - component.previewUrl = "https://example.com/file.pdf"; + it('should show open button when previewUrl exists', () => { + component.previewUrl = 'https://example.com/file.pdf'; fixture.detectChanges(); - const buttons = fixture.nativeElement.querySelectorAll("button"); - const openButton = Array.from(buttons).find((b: any) => - b.textContent.includes("Open"), - ); + const buttons = fixture.nativeElement.querySelectorAll('button'); + const openButton = Array.from(buttons).find((b: any) => b.textContent.includes('Open')); expect(openButton).toBeTruthy(); }); - it("should not show open button when previewUrl is null", () => { + it('should not show open button when previewUrl is null', () => { component.previewUrl = null; fixture.detectChanges(); - const buttons = fixture.nativeElement.querySelectorAll("button"); - const openButton = Array.from(buttons).find((b: any) => - b.textContent.includes("Open"), - ); + const buttons = fixture.nativeElement.querySelectorAll('button'); + const openButton = Array.from(buttons).find((b: any) => b.textContent.includes('Open')); expect(openButton).toBeFalsy(); }); - it("should show spinner when loading", () => { - component.value = "uploads/file.pdf"; + it('should show spinner when loading', () => { + component.value = 'uploads/file.pdf'; component.isLoading = true; fixture.detectChanges(); - const spinner = fixture.nativeElement.querySelector("mat-spinner"); + const spinner = fixture.nativeElement.querySelector('mat-spinner'); expect(spinner).toBeTruthy(); }); - it("should show image preview for image files", () => { - component.value = "uploads/photo.jpg"; + it('should show image preview for image files', () => { + component.value = 'uploads/photo.jpg'; component.isImage = true; - component.previewUrl = "https://example.com/photo.jpg"; + component.previewUrl = 'https://example.com/photo.jpg'; component.isLoading = false; fixture.detectChanges(); - const img = fixture.nativeElement.querySelector(".s3-widget__thumbnail"); + const img = fixture.nativeElement.querySelector('.s3-widget__thumbnail'); expect(img).toBeTruthy(); - expect(img.src).toBe("https://example.com/photo.jpg"); + expect(img.src).toBe('https://example.com/photo.jpg'); }); - it("should show file icon for non-image files", () => { - component.value = "uploads/document.pdf"; + it('should show file icon for non-image files', () => { + component.value = 'uploads/document.pdf'; component.isImage = false; - component.previewUrl = "https://example.com/document.pdf"; + component.previewUrl = 'https://example.com/document.pdf'; component.isLoading = false; fixture.detectChanges(); - const fileIcon = fixture.nativeElement.querySelector( - ".s3-widget__file-icon", - ); + const fileIcon = fixture.nativeElement.querySelector('.s3-widget__file-icon'); expect(fileIcon).toBeTruthy(); }); - it("should show truncated filename for long filenames", () => { - component.value = - "uploads/very-long-filename-that-should-be-truncated.pdf"; + it('should show truncated filename for long filenames', () => { + component.value = 'uploads/very-long-filename-that-should-be-truncated.pdf'; component.isImage = false; - component.previewUrl = "https://example.com/file.pdf"; + component.previewUrl = 'https://example.com/file.pdf'; component.isLoading = false; fixture.detectChanges(); - const filename = fixture.nativeElement.querySelector( - ".s3-widget__filename", - ); + const filename = fixture.nativeElement.querySelector('.s3-widget__filename'); expect(filename).toBeTruthy(); }); }); diff --git a/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.spec.ts index 3654cf088..1bf8e97fb 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/timezone/timezone.component.spec.ts @@ -4,49 +4,49 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { TimezoneEditComponent } from './timezone.component'; describe('TimezoneEditComponent', () => { - let component: TimezoneEditComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TimezoneEditComponent, BrowserAnimationsModule] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(TimezoneEditComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should populate timezones using Intl API', () => { - expect(component.timezones.length).toBeGreaterThan(0); - }); - - it('should include timezone offset in labels', () => { - const timezone = component.timezones.find(tz => tz.value === 'America/New_York'); - expect(timezone).toBeDefined(); - expect(timezone.label).toContain('UTC'); - }); - - it('should emit value on change', () => { - vi.spyOn(component.onFieldChange, 'emit'); - const testValue = 'America/New_York'; - component.value = testValue; - component.onFieldChange.emit(testValue); - expect(component.onFieldChange.emit).toHaveBeenCalledWith(testValue); - }); - - it('should add null option when allow_null is true', () => { - component.widgetStructure = { - widget_params: { allow_null: true } - } as any; - component.ngOnInit(); - const nullOption = component.timezones.find(tz => tz.value === null); - expect(nullOption).toBeDefined(); - }); + let component: TimezoneEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TimezoneEditComponent, BrowserAnimationsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TimezoneEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should populate timezones using Intl API', () => { + expect(component.timezones.length).toBeGreaterThan(0); + }); + + it('should include timezone offset in labels', () => { + const timezone = component.timezones.find((tz) => tz.value === 'America/New_York'); + expect(timezone).toBeDefined(); + expect(timezone.label).toContain('UTC'); + }); + + it('should emit value on change', () => { + vi.spyOn(component.onFieldChange, 'emit'); + const testValue = 'America/New_York'; + component.value = testValue; + component.onFieldChange.emit(testValue); + expect(component.onFieldChange.emit).toHaveBeenCalledWith(testValue); + }); + + it('should add null option when allow_null is true', () => { + component.widgetStructure = { + widget_params: { allow_null: true }, + } as any; + component.ngOnInit(); + const nullOption = component.timezones.find((tz) => tz.value === null); + expect(nullOption).toBeDefined(); + }); }); diff --git a/frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.spec.ts b/frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.spec.ts index d19ffb75c..b2fd2598e 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.spec.ts +++ b/frontend/src/app/components/ui-components/table-display-fields/timezone/timezone.component.spec.ts @@ -2,41 +2,41 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TimezoneDisplayComponent } from './timezone.component'; describe('TimezoneDisplayComponent', () => { - let component: TimezoneDisplayComponent; - let fixture: ComponentFixture; + let component: TimezoneDisplayComponent; + let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TimezoneDisplayComponent] - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TimezoneDisplayComponent], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(TimezoneDisplayComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(TimezoneDisplayComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should display formatted timezone with UTC offset', () => { - component.value = 'America/New_York'; - expect(component.formattedTimezone).toContain('America/New_York'); - expect(component.formattedTimezone).toContain('UTC'); - }); + it('should display formatted timezone with UTC offset', () => { + component.value = 'America/New_York'; + expect(component.formattedTimezone).toContain('America/New_York'); + expect(component.formattedTimezone).toContain('UTC'); + }); - it('should display dash for null value', () => { - component.value = null; - expect(component.formattedTimezone).toBe('—'); - }); + it('should display dash for null value', () => { + component.value = null; + expect(component.formattedTimezone).toBe('—'); + }); - it('should emit copy event on button click', () => { - vi.spyOn(component.onCopyToClipboard, 'emit'); - component.value = 'Europe/London'; - const compiled = fixture.nativeElement; - const button = compiled.querySelector('button'); - expect(button).toBeTruthy(); - }); + it('should emit copy event on button click', () => { + vi.spyOn(component.onCopyToClipboard, 'emit'); + component.value = 'Europe/London'; + const compiled = fixture.nativeElement; + const button = compiled.querySelector('button'); + expect(button).toBeTruthy(); + }); }); diff --git a/frontend/src/app/components/ui-components/turnstile/turnstile.component.css b/frontend/src/app/components/ui-components/turnstile/turnstile.component.css new file mode 100644 index 000000000..46f2ea33b --- /dev/null +++ b/frontend/src/app/components/ui-components/turnstile/turnstile.component.css @@ -0,0 +1,5 @@ +.turnstile-container { + display: flex; + justify-content: center; + margin: 16px 0; +} diff --git a/frontend/src/app/components/ui-components/turnstile/turnstile.component.html b/frontend/src/app/components/ui-components/turnstile/turnstile.component.html new file mode 100644 index 000000000..515fd4bd4 --- /dev/null +++ b/frontend/src/app/components/ui-components/turnstile/turnstile.component.html @@ -0,0 +1 @@ +

diff --git a/frontend/src/app/components/ui-components/turnstile/turnstile.component.spec.ts b/frontend/src/app/components/ui-components/turnstile/turnstile.component.spec.ts new file mode 100644 index 000000000..fb6ab7bdc --- /dev/null +++ b/frontend/src/app/components/ui-components/turnstile/turnstile.component.spec.ts @@ -0,0 +1,133 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TurnstileComponent } from './turnstile.component'; + +describe('TurnstileComponent', () => { + let component: TurnstileComponent; + let fixture: ComponentFixture; + let mockWidgetId: string; + + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + beforeEach(async () => { + mockWidgetId = 'mock-widget-id'; + + (window as any).turnstile = { + render: vi.fn().mockReturnValue(mockWidgetId), + reset: vi.fn(), + getResponse: vi.fn(), + remove: vi.fn(), + }; + + await TestBed.configureTestingModule({ + imports: [TurnstileComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TurnstileComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + component.ngOnDestroy(); + delete (window as any).turnstile; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should render turnstile widget on init', async () => { + fixture.detectChanges(); + await delay(150); + + expect((window as any).turnstile?.render).toHaveBeenCalled(); + }); + + it('should emit tokenReceived when callback is triggered', async () => { + let receivedToken: string | null = null; + component.tokenReceived.subscribe((token: string) => { + receivedToken = token; + }); + + ((window as any).turnstile?.render as ReturnType).mockImplementation( + (_container: any, options: any) => { + options.callback('test-token'); + return mockWidgetId; + }, + ); + + fixture.detectChanges(); + await delay(150); + + expect(receivedToken).toBe('test-token'); + }); + + it('should emit tokenError when error-callback is triggered', async () => { + let errorEmitted = false; + component.tokenError.subscribe(() => { + errorEmitted = true; + }); + + ((window as any).turnstile?.render as ReturnType).mockImplementation( + (_container: any, options: any) => { + options['error-callback'](); + return mockWidgetId; + }, + ); + + fixture.detectChanges(); + await delay(150); + + expect(errorEmitted).toBe(true); + }); + + it('should emit tokenExpired when expired-callback is triggered', async () => { + let expiredEmitted = false; + component.tokenExpired.subscribe(() => { + expiredEmitted = true; + }); + + ((window as any).turnstile?.render as ReturnType).mockImplementation( + (_container: any, options: any) => { + options['expired-callback'](); + return mockWidgetId; + }, + ); + + fixture.detectChanges(); + await delay(150); + + expect(expiredEmitted).toBe(true); + }); + + it('should reset the widget when reset() is called', async () => { + fixture.detectChanges(); + await delay(150); + + component.reset(); + + expect((window as any).turnstile?.reset).toHaveBeenCalledWith(mockWidgetId); + }); + + it('should remove widget on destroy', async () => { + fixture.detectChanges(); + await delay(150); + + component.ngOnDestroy(); + + expect((window as any).turnstile?.remove).toHaveBeenCalledWith(mockWidgetId); + }); + + it('should emit error if turnstile fails to load', async () => { + delete (window as any).turnstile; + let errorEmitted = false; + component.tokenError.subscribe(() => { + errorEmitted = true; + }); + + fixture.detectChanges(); + // MAX_POLL_ATTEMPTS (50) * POLL_INTERVAL_MS (100) = 5000ms + await delay(5200); + + expect(errorEmitted).toBe(true); + }, 10000); +}); diff --git a/frontend/src/app/components/ui-components/turnstile/turnstile.component.ts b/frontend/src/app/components/ui-components/turnstile/turnstile.component.ts new file mode 100644 index 000000000..73ff0cbef --- /dev/null +++ b/frontend/src/app/components/ui-components/turnstile/turnstile.component.ts @@ -0,0 +1,95 @@ +import { CommonModule } from '@angular/common'; +import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; +import { environment } from 'src/environments/environment'; + +@Component({ + selector: 'app-turnstile', + templateUrl: './turnstile.component.html', + styleUrls: ['./turnstile.component.css'], + imports: [CommonModule], +}) +export class TurnstileComponent implements OnInit, OnDestroy { + @ViewChild('turnstileContainer', { static: true }) turnstileContainer: ElementRef; + + @Input() siteKey: string = (environment as any).turnstileSiteKey; + @Input() theme: 'light' | 'dark' | 'auto' = 'auto'; + + @Output() tokenReceived = new EventEmitter(); + @Output() tokenError = new EventEmitter(); + @Output() tokenExpired = new EventEmitter(); + + private widgetId: string | null = null; + private pollInterval: ReturnType | null = null; + private readonly MAX_POLL_ATTEMPTS = 50; + private readonly POLL_INTERVAL_MS = 100; + + ngOnInit(): void { + this._waitForTurnstileAndRender(); + } + + ngOnDestroy(): void { + this._clearPollInterval(); + this._removeWidget(); + } + + public reset(): void { + if (window.turnstile && this.widgetId) { + window.turnstile.reset(this.widgetId); + } + } + + private _waitForTurnstileAndRender(): void { + let attempts = 0; + + this.pollInterval = setInterval(() => { + attempts++; + + if (window.turnstile) { + this._clearPollInterval(); + this._renderWidget(); + return; + } + + if (attempts >= this.MAX_POLL_ATTEMPTS) { + this._clearPollInterval(); + console.error('Turnstile script failed to load'); + this.tokenError.emit(); + } + }, this.POLL_INTERVAL_MS); + } + + private _renderWidget(): void { + if (!window.turnstile || !this.turnstileContainer?.nativeElement) { + return; + } + + this.widgetId = window.turnstile.render(this.turnstileContainer.nativeElement, { + sitekey: this.siteKey, + callback: (token: string) => { + this.tokenReceived.emit(token); + }, + 'error-callback': () => { + this.tokenError.emit(); + }, + 'expired-callback': () => { + this.tokenExpired.emit(); + }, + theme: this.theme, + appearance: 'always', + }); + } + + private _removeWidget(): void { + if (window.turnstile && this.widgetId) { + window.turnstile.remove(this.widgetId); + this.widgetId = null; + } + } + + private _clearPollInterval(): void { + if (this.pollInterval) { + clearInterval(this.pollInterval); + this.pollInterval = null; + } + } +} diff --git a/frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.spec.ts b/frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.spec.ts index 929be0630..00b6d07b8 100644 --- a/frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.spec.ts +++ b/frontend/src/app/components/users/group-add-dialog/group-add-dialog.component.spec.ts @@ -1,65 +1,65 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule, } from '@angular/material/dialog'; -import { FormsModule } from '@angular/forms'; -import { GroupAddDialogComponent } from './group-add-dialog.component'; +import { FormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { UsersService } from 'src/app/services/users.service'; -import { of } from 'rxjs'; -import { Angulartics2Module } from 'angulartics2'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { provideHttpClient } from '@angular/common/http'; import { provideRouter } from '@angular/router'; +import { Angulartics2Module } from 'angulartics2'; +import { of } from 'rxjs'; +import { UsersService } from 'src/app/services/users.service'; +import { GroupAddDialogComponent } from './group-add-dialog.component'; describe('GroupAddDialogComponent', () => { - let component: GroupAddDialogComponent; - let fixture: ComponentFixture; - let usersService: UsersService; + let component: GroupAddDialogComponent; + let fixture: ComponentFixture; + let usersService: UsersService; - const mockDialogRef = { - close: () => { } - }; + const mockDialogRef = { + close: () => {}, + }; - beforeEach(async() => { - TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - FormsModule, - MatDialogModule, - Angulartics2Module.forRoot({}), - GroupAddDialogComponent, - BrowserAnimationsModule - ], - providers: [ - provideHttpClient(), - provideRouter([]), - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: MatDialogRef, useValue: mockDialogRef }, - ], - }).compileComponents(); - }); + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [ + MatSnackBarModule, + FormsModule, + MatDialogModule, + Angulartics2Module.forRoot({}), + GroupAddDialogComponent, + BrowserAnimationsModule, + ], + providers: [ + provideHttpClient(), + provideRouter([]), + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: MatDialogRef, useValue: mockDialogRef }, + ], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(GroupAddDialogComponent); - component = fixture.componentInstance; - usersService = TestBed.inject(UsersService); - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(GroupAddDialogComponent); + component = fixture.componentInstance; + usersService = TestBed.inject(UsersService); + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should call create user group service', () => { - component.groupTitle = 'Sellers'; - component.connectionID = '12345678'; - const fakeCreateUsersGroup = vi.spyOn(usersService, 'createUsersGroup').mockReturnValue(of()); - vi.spyOn(mockDialogRef, 'close'); + it('should call create user group service', () => { + component.groupTitle = 'Sellers'; + component.connectionID = '12345678'; + const fakeCreateUsersGroup = vi.spyOn(usersService, 'createUsersGroup').mockReturnValue(of()); + vi.spyOn(mockDialogRef, 'close'); - component.addGroup(); + component.addGroup(); - expect(fakeCreateUsersGroup).toHaveBeenCalledWith('12345678', 'Sellers'); - // expect(component.dialogRef.close).toHaveBeenCalled(); - expect(component.submitting).toBe(false); - }); + expect(fakeCreateUsersGroup).toHaveBeenCalledWith('12345678', 'Sellers'); + // expect(component.dialogRef.close).toHaveBeenCalled(); + expect(component.submitting).toBe(false); + }); }); diff --git a/frontend/src/app/components/users/group-delete-dialog/group-delete-dialog.component.spec.ts b/frontend/src/app/components/users/group-delete-dialog/group-delete-dialog.component.spec.ts index 9c163191e..9b5b32e94 100644 --- a/frontend/src/app/components/users/group-delete-dialog/group-delete-dialog.component.spec.ts +++ b/frontend/src/app/components/users/group-delete-dialog/group-delete-dialog.component.spec.ts @@ -1,55 +1,50 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; - -import { GroupDeleteDialogComponent } from './group-delete-dialog.component'; -import { UsersService } from 'src/app/services/users.service'; -import { of } from 'rxjs'; import { Angulartics2Module } from 'angulartics2'; -import { provideHttpClient } from '@angular/common/http'; +import { of } from 'rxjs'; +import { UsersService } from 'src/app/services/users.service'; +import { GroupDeleteDialogComponent } from './group-delete-dialog.component'; describe('GroupDeleteDialogComponent', () => { - let component: GroupDeleteDialogComponent; - let fixture: ComponentFixture; - let usersService: UsersService; - - const mockDialogRef = { - close: () => { } - }; - - beforeEach(async() => { - TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - Angulartics2Module.forRoot(), - GroupDeleteDialogComponent - ], - providers: [ - provideHttpClient(), - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: MatDialogRef, useValue: mockDialogRef }, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(GroupDeleteDialogComponent); - component = fixture.componentInstance; - usersService = TestBed.inject(UsersService); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should call delete user group service', () => { - const fakeDeleteUsersGroup = vi.spyOn(usersService, 'deleteUsersGroup').mockReturnValue(of()); - vi.spyOn(mockDialogRef, 'close'); - - component.deleteUsersGroup('12345678-123'); - expect(fakeDeleteUsersGroup).toHaveBeenCalledWith('12345678-123'); - // expect(component.dialogRef.close).toHaveBeenCalled(); - expect(component.submitting).toBe(false); - }); + let component: GroupDeleteDialogComponent; + let fixture: ComponentFixture; + let usersService: UsersService; + + const mockDialogRef = { + close: () => {}, + }; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MatSnackBarModule, Angulartics2Module.forRoot(), GroupDeleteDialogComponent], + providers: [ + provideHttpClient(), + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: MatDialogRef, useValue: mockDialogRef }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupDeleteDialogComponent); + component = fixture.componentInstance; + usersService = TestBed.inject(UsersService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call delete user group service', () => { + const fakeDeleteUsersGroup = vi.spyOn(usersService, 'deleteUsersGroup').mockReturnValue(of()); + vi.spyOn(mockDialogRef, 'close'); + + component.deleteUsersGroup('12345678-123'); + expect(fakeDeleteUsersGroup).toHaveBeenCalledWith('12345678-123'); + // expect(component.dialogRef.close).toHaveBeenCalled(); + expect(component.submitting).toBe(false); + }); }); diff --git a/frontend/src/app/components/users/permissions-add-dialog/permissions-add-dialog.component.spec.ts b/frontend/src/app/components/users/permissions-add-dialog/permissions-add-dialog.component.spec.ts index a21705b1f..322eff0ce 100644 --- a/frontend/src/app/components/users/permissions-add-dialog/permissions-add-dialog.component.spec.ts +++ b/frontend/src/app/components/users/permissions-add-dialog/permissions-add-dialog.component.spec.ts @@ -1,306 +1,299 @@ +import { provideHttpClient } from '@angular/common/http'; +import { forwardRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatRadioModule } from '@angular/material/radio'; - -import { PermissionsAddDialogComponent } from './permissions-add-dialog.component'; -import { forwardRef } from '@angular/core'; -import { UsersService } from 'src/app/services/users.service'; -import { of } from 'rxjs'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { AccessLevel } from 'src/app/models/user'; -import { Angulartics2Module } from 'angulartics2'; -import { provideHttpClient } from '@angular/common/http'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { provideRouter } from '@angular/router'; +import { Angulartics2Module } from 'angulartics2'; +import { of } from 'rxjs'; +import { AccessLevel } from 'src/app/models/user'; +import { UsersService } from 'src/app/services/users.service'; +import { PermissionsAddDialogComponent } from './permissions-add-dialog.component'; describe('PermissionsAddDialogComponent', () => { - let component: PermissionsAddDialogComponent; - let fixture: ComponentFixture; - let usersService: UsersService; - - const mockDialogRef = { - close: () => { } - }; - - const fakeCustomersPermissionsResponse = { - "tableName": "customers", - "accessLevel": { - "visibility": true, - "readonly": false, - "add": true, - "delete": false, - "edit": true - } - } - - const fakeCustomersPermissionsApp = { - tableName: "customers", - display_name: "Customers", - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: false, - edit: true - } - } - - const fakeOrdersPermissionsResponse = { - "tableName": "orders", - "display_name": "Created orders", - "accessLevel": { - "visibility": false, - "readonly": false, - "add": false, - "delete": false, - "edit": false - } - } - - const fakeOrdersPermissionsApp = { - tableName: "orders", - display_name: "Created orders", - accessLevel: { - visibility: false, - readonly: false, - add: false, - delete: false, - edit: false - } - } - - const fakeTablePermissionsResponse = [ - fakeCustomersPermissionsResponse, - fakeOrdersPermissionsResponse - ]; - - const fakeTablePermissionsApp = [ - fakeCustomersPermissionsApp, - fakeOrdersPermissionsApp - ]; - - const fakePermissionsResponse = { - "connection": { - "connectionId": "5e1092f8-4e50-4e6c-bad9-bd0b04d1af2a", - "accessLevel": "readonly" - }, - "group": { - "groupId": "77154868-eaf0-4a53-9693-0470182d0971", - "accessLevel": "edit" - }, - "tables": fakeTablePermissionsResponse - } - - beforeEach(async() => { - await TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - FormsModule, - MatRadioModule, - MatSlideToggleModule, - MatCheckboxModule, - MatDialogModule, - BrowserAnimationsModule, - Angulartics2Module.forRoot(), - PermissionsAddDialogComponent - ], - providers: [ - provideHttpClient(), - provideRouter([]), - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: MatDialogRef, useValue: mockDialogRef }, - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => PermissionsAddDialogComponent), - multi: true - } - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(PermissionsAddDialogComponent); - component = fixture.componentInstance; - usersService = TestBed.inject(UsersService); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should set initial state of permissions', async () => { - vi.spyOn(usersService, 'fetchPermission').mockReturnValue(of(fakePermissionsResponse)); - - component.ngOnInit(); - fixture.detectChanges(); - await fixture.whenStable(); - - // crutch, i don't like it - component.tablesAccess = [...fakeTablePermissionsApp]; - - expect(component.connectionAccess).toEqual('readonly'); - expect(component.groupAccess).toEqual('edit'); - expect(component.tablesAccess).toEqual(fakeTablePermissionsApp); - }); - - it('should uncheck actions if table is readonly', () => { - component.tablesAccess = [...fakeTablePermissionsApp]; - - component.uncheckActions(component.tablesAccess[0]); - - expect(component.tablesAccess[0].accessLevel.readonly).toBe(false); - expect(component.tablesAccess[0].accessLevel.add).toBe(false); - expect(component.tablesAccess[0].accessLevel.delete).toBe(false); - expect(component.tablesAccess[0].accessLevel.edit).toBe(false); - }); - - it('should uncheck actions if table is invisible', () => { - component.tablesAccess = [...fakeTablePermissionsApp]; - - component.uncheckActions(component.tablesAccess[1]); - - expect(component.tablesAccess[1].accessLevel.readonly).toBe(false); - expect(component.tablesAccess[1].accessLevel.add).toBe(false); - expect(component.tablesAccess[1].accessLevel.delete).toBe(false); - expect(component.tablesAccess[1].accessLevel.edit).toBe(false); - }); - - it('should select all tables', () => { - component.tablesAccess = [...fakeTablePermissionsApp]; - - component.grantFullTableAccess(); - - expect(component.tablesAccess).toEqual([ - { - tableName: "customers", - display_name: "Customers", - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true - } - }, - { - tableName: "orders", - display_name: "Created orders", - accessLevel: { - visibility: true, - readonly: false, - add: true, - delete: true, - edit: true - } - } - ]) - }); - - it('should deselect all tables', () => { - component.tablesAccess = [...fakeTablePermissionsApp]; - - component.deselectAllTables(); - - expect(component.tablesAccess).toEqual([ - { - tableName: "customers", - display_name: "Customers", - accessLevel: { - visibility: false, - readonly: false, - add: false, - delete: false, - edit: false - } - }, - { - tableName: "orders", - display_name: "Created orders", - accessLevel: { - visibility: false, - readonly: false, - add: false, - delete: false, - edit: false - } - } - ]) - }); - - it('should call add permissions service', () => { - component.connectionID = '12345678'; - component.connectionAccess = AccessLevel.Readonly; - component.group.id = "12345678-123"; - component.groupAccess = AccessLevel.Edit; - component.tablesAccess = [ - { - "tableName": "customers", - display_name: "Customers", - "accessLevel": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - } - }, - { - "tableName": "orders", - display_name: "Created orders", - "accessLevel": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - } - } - ]; - - const fakseUpdatePermission = vi.spyOn(usersService, 'updatePermission').mockReturnValue(of()); - vi.spyOn(mockDialogRef, 'close'); - - component.addPermissions(); - - expect(fakseUpdatePermission).toHaveBeenCalledWith('12345678', { - connection: { - connectionId: '12345678', - accessLevel: AccessLevel.Readonly - }, - group: { - groupId: '12345678-123', - accessLevel: AccessLevel.Edit - }, - tables: [ - { - "tableName": "customers", - display_name: "Customers", - "accessLevel": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - } - }, - { - "tableName": "orders", - display_name: "Created orders", - "accessLevel": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - } - } - ] - }); - // expect(component.dialogRef.close).toHaveBeenCalled(); - expect(component.submitting).toBe(false); - }) + let component: PermissionsAddDialogComponent; + let fixture: ComponentFixture; + let usersService: UsersService; + + const mockDialogRef = { + close: () => {}, + }; + + const fakeCustomersPermissionsResponse = { + tableName: 'customers', + accessLevel: { + visibility: true, + readonly: false, + add: true, + delete: false, + edit: true, + }, + }; + + const fakeCustomersPermissionsApp = { + tableName: 'customers', + display_name: 'Customers', + accessLevel: { + visibility: true, + readonly: false, + add: true, + delete: false, + edit: true, + }, + }; + + const fakeOrdersPermissionsResponse = { + tableName: 'orders', + display_name: 'Created orders', + accessLevel: { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + }, + }; + + const fakeOrdersPermissionsApp = { + tableName: 'orders', + display_name: 'Created orders', + accessLevel: { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + }, + }; + + const fakeTablePermissionsResponse = [fakeCustomersPermissionsResponse, fakeOrdersPermissionsResponse]; + + const fakeTablePermissionsApp = [fakeCustomersPermissionsApp, fakeOrdersPermissionsApp]; + + const fakePermissionsResponse = { + connection: { + connectionId: '5e1092f8-4e50-4e6c-bad9-bd0b04d1af2a', + accessLevel: 'readonly', + }, + group: { + groupId: '77154868-eaf0-4a53-9693-0470182d0971', + accessLevel: 'edit', + }, + tables: fakeTablePermissionsResponse, + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MatSnackBarModule, + FormsModule, + MatRadioModule, + MatSlideToggleModule, + MatCheckboxModule, + MatDialogModule, + BrowserAnimationsModule, + Angulartics2Module.forRoot(), + PermissionsAddDialogComponent, + ], + providers: [ + provideHttpClient(), + provideRouter([]), + { provide: MAT_DIALOG_DATA, useValue: {} }, + { provide: MatDialogRef, useValue: mockDialogRef }, + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PermissionsAddDialogComponent), + multi: true, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PermissionsAddDialogComponent); + component = fixture.componentInstance; + usersService = TestBed.inject(UsersService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set initial state of permissions', async () => { + vi.spyOn(usersService, 'fetchPermission').mockReturnValue(of(fakePermissionsResponse)); + + component.ngOnInit(); + fixture.detectChanges(); + await fixture.whenStable(); + + // crutch, i don't like it + component.tablesAccess = [...fakeTablePermissionsApp]; + + expect(component.connectionAccess).toEqual('readonly'); + expect(component.groupAccess).toEqual('edit'); + expect(component.tablesAccess).toEqual(fakeTablePermissionsApp); + }); + + it('should uncheck actions if table is readonly', () => { + component.tablesAccess = [...fakeTablePermissionsApp]; + + component.uncheckActions(component.tablesAccess[0]); + + expect(component.tablesAccess[0].accessLevel.readonly).toBe(false); + expect(component.tablesAccess[0].accessLevel.add).toBe(false); + expect(component.tablesAccess[0].accessLevel.delete).toBe(false); + expect(component.tablesAccess[0].accessLevel.edit).toBe(false); + }); + + it('should uncheck actions if table is invisible', () => { + component.tablesAccess = [...fakeTablePermissionsApp]; + + component.uncheckActions(component.tablesAccess[1]); + + expect(component.tablesAccess[1].accessLevel.readonly).toBe(false); + expect(component.tablesAccess[1].accessLevel.add).toBe(false); + expect(component.tablesAccess[1].accessLevel.delete).toBe(false); + expect(component.tablesAccess[1].accessLevel.edit).toBe(false); + }); + + it('should select all tables', () => { + component.tablesAccess = [...fakeTablePermissionsApp]; + + component.grantFullTableAccess(); + + expect(component.tablesAccess).toEqual([ + { + tableName: 'customers', + display_name: 'Customers', + accessLevel: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + }, + { + tableName: 'orders', + display_name: 'Created orders', + accessLevel: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + }, + ]); + }); + + it('should deselect all tables', () => { + component.tablesAccess = [...fakeTablePermissionsApp]; + + component.deselectAllTables(); + + expect(component.tablesAccess).toEqual([ + { + tableName: 'customers', + display_name: 'Customers', + accessLevel: { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + }, + }, + { + tableName: 'orders', + display_name: 'Created orders', + accessLevel: { + visibility: false, + readonly: false, + add: false, + delete: false, + edit: false, + }, + }, + ]); + }); + + it('should call add permissions service', () => { + component.connectionID = '12345678'; + component.connectionAccess = AccessLevel.Readonly; + component.group.id = '12345678-123'; + component.groupAccess = AccessLevel.Edit; + component.tablesAccess = [ + { + tableName: 'customers', + display_name: 'Customers', + accessLevel: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + }, + { + tableName: 'orders', + display_name: 'Created orders', + accessLevel: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + }, + ]; + + const fakseUpdatePermission = vi.spyOn(usersService, 'updatePermission').mockReturnValue(of()); + vi.spyOn(mockDialogRef, 'close'); + + component.addPermissions(); + + expect(fakseUpdatePermission).toHaveBeenCalledWith('12345678', { + connection: { + connectionId: '12345678', + accessLevel: AccessLevel.Readonly, + }, + group: { + groupId: '12345678-123', + accessLevel: AccessLevel.Edit, + }, + tables: [ + { + tableName: 'customers', + display_name: 'Customers', + accessLevel: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + }, + { + tableName: 'orders', + display_name: 'Created orders', + accessLevel: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + }, + ], + }); + // expect(component.dialogRef.close).toHaveBeenCalled(); + expect(component.submitting).toBe(false); + }); }); diff --git a/frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.spec.ts b/frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.spec.ts index 384336621..af8a5ce81 100644 --- a/frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.spec.ts +++ b/frontend/src/app/components/users/user-add-dialog/user-add-dialog.component.spec.ts @@ -1,70 +1,72 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; - -import { FormsModule } from '@angular/forms'; -import { UserAddDialogComponent } from './user-add-dialog.component'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Angulartics2Module } from 'angulartics2'; import { of } from 'rxjs'; import { UsersService } from 'src/app/services/users.service'; -import { Angulartics2Module } from 'angulartics2'; -import { provideHttpClient } from '@angular/common/http'; -import { RouterTestingModule } from '@angular/router/testing'; +import { UserAddDialogComponent } from './user-add-dialog.component'; describe('UserAddDialogComponent', () => { - let component: UserAddDialogComponent; - let fixture: ComponentFixture; - let usersService: UsersService; + let component: UserAddDialogComponent; + let fixture: ComponentFixture; + let usersService: UsersService; - const mockDialogRef = { - close: () => { } - }; + const mockDialogRef = { + close: () => {}, + }; - beforeEach(async() => { - await TestBed.configureTestingModule({ - imports: [ - RouterTestingModule, - MatSnackBarModule, - FormsModule, - Angulartics2Module.forRoot(), - UserAddDialogComponent - ], - providers: [ - provideHttpClient(), - { provide: MAT_DIALOG_DATA, useValue: { - availableMembers: [], - group: { - id: '12345678-123', - title: 'Test Group' - } - }}, - { provide: MatDialogRef, useValue: mockDialogRef } - ], - }).compileComponents(); - }); + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + RouterTestingModule, + MatSnackBarModule, + FormsModule, + Angulartics2Module.forRoot(), + UserAddDialogComponent, + ], + providers: [ + provideHttpClient(), + { + provide: MAT_DIALOG_DATA, + useValue: { + availableMembers: [], + group: { + id: '12345678-123', + title: 'Test Group', + }, + }, + }, + { provide: MatDialogRef, useValue: mockDialogRef }, + ], + }).compileComponents(); + }); - beforeEach(() => { - fixture = TestBed.createComponent(UserAddDialogComponent); - component = fixture.componentInstance; - usersService = TestBed.inject(UsersService); - fixture.detectChanges(); - }); + beforeEach(() => { + fixture = TestBed.createComponent(UserAddDialogComponent); + component = fixture.componentInstance; + usersService = TestBed.inject(UsersService); + fixture.detectChanges(); + }); - it('should create', () => { - expect(component).toBeTruthy(); - }); + it('should create', () => { + expect(component).toBeTruthy(); + }); - it('should call add user service', () => { - component.groupUserEmail = 'user@test.com'; - const fakeAddUser = vi.spyOn(usersService, 'addGroupUser').mockReturnValue(of()); - // vi.spyOn(mockDialogRef, 'close'); + it('should call add user service', () => { + component.groupUserEmail = 'user@test.com'; + const fakeAddUser = vi.spyOn(usersService, 'addGroupUser').mockReturnValue(of()); + // vi.spyOn(mockDialogRef, 'close'); - component.joinGroupUser(); - expect(fakeAddUser).toHaveBeenCalledWith('12345678-123', 'user@test.com'); + component.joinGroupUser(); + expect(fakeAddUser).toHaveBeenCalledWith('12345678-123', 'user@test.com'); - // fixture.detectChanges(); - // fixture.whenStable().then(() => { - // expect(component.dialogRef.close).toHaveBeenCalled(); - // expect(component.submitting).toBe(false); - // }); - }); + // fixture.detectChanges(); + // fixture.whenStable().then(() => { + // expect(component.dialogRef.close).toHaveBeenCalled(); + // expect(component.submitting).toBe(false); + // }); + }); }); diff --git a/frontend/src/app/components/users/user-delete-dialog/user-delete-dialog.component.spec.ts b/frontend/src/app/components/users/user-delete-dialog/user-delete-dialog.component.spec.ts index 97cb02293..40c0bb7e4 100644 --- a/frontend/src/app/components/users/user-delete-dialog/user-delete-dialog.component.spec.ts +++ b/frontend/src/app/components/users/user-delete-dialog/user-delete-dialog.component.spec.ts @@ -1,50 +1,49 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; - -import { UserDeleteDialogComponent } from './user-delete-dialog.component'; -import { UsersService } from 'src/app/services/users.service'; import { of } from 'rxjs'; -import { provideHttpClient } from '@angular/common/http'; +import { UsersService } from 'src/app/services/users.service'; +import { UserDeleteDialogComponent } from './user-delete-dialog.component'; describe('UserDeleteDialogComponent', () => { - let component: UserDeleteDialogComponent; - let fixture: ComponentFixture; - let usersService: UsersService; - - const mockDialogRef = { - close: () => { } - }; - - beforeEach(async() => { - await TestBed.configureTestingModule({ - imports: [MatSnackBarModule, UserDeleteDialogComponent], - providers: [ - provideHttpClient(), - { provide: MAT_DIALOG_DATA, useValue: { user: { email: 'user@test.com' }, group: { id: '12345678-123' } } }, - { provide: MatDialogRef, useValue: mockDialogRef }, - ] - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(UserDeleteDialogComponent); - component = fixture.componentInstance; - usersService = TestBed.inject(UsersService); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should call delete user service', () => { - const fakeDeleteUser = vi.spyOn(usersService, 'deleteGroupUser').mockReturnValue(of()); - vi.spyOn(mockDialogRef, 'close'); - - component.deleteGroupUser(); - expect(fakeDeleteUser).toHaveBeenCalledWith('user@test.com', '12345678-123'); - // expect(component.dialogRef.close).toHaveBeenCalled(); - expect(component.submitting).toBe(false); - }); + let component: UserDeleteDialogComponent; + let fixture: ComponentFixture; + let usersService: UsersService; + + const mockDialogRef = { + close: () => {}, + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MatSnackBarModule, UserDeleteDialogComponent], + providers: [ + provideHttpClient(), + { provide: MAT_DIALOG_DATA, useValue: { user: { email: 'user@test.com' }, group: { id: '12345678-123' } } }, + { provide: MatDialogRef, useValue: mockDialogRef }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UserDeleteDialogComponent); + component = fixture.componentInstance; + usersService = TestBed.inject(UsersService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call delete user service', () => { + const fakeDeleteUser = vi.spyOn(usersService, 'deleteGroupUser').mockReturnValue(of()); + vi.spyOn(mockDialogRef, 'close'); + + component.deleteGroupUser(); + expect(fakeDeleteUser).toHaveBeenCalledWith('user@test.com', '12345678-123'); + // expect(component.dialogRef.close).toHaveBeenCalled(); + expect(component.submitting).toBe(false); + }); }); diff --git a/frontend/src/app/components/users/users.component.spec.ts b/frontend/src/app/components/users/users.component.spec.ts index 9021a5830..1f42e1009 100644 --- a/frontend/src/app/components/users/users.component.spec.ts +++ b/frontend/src/app/components/users/users.component.spec.ts @@ -1,197 +1,187 @@ +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; - +import { provideRouter } from '@angular/router'; +import { Angulartics2Module } from 'angulartics2'; +import { of } from 'rxjs'; +import { UsersService } from 'src/app/services/users.service'; import { GroupAddDialogComponent } from './group-add-dialog/group-add-dialog.component'; import { GroupDeleteDialogComponent } from './group-delete-dialog/group-delete-dialog.component'; import { PermissionsAddDialogComponent } from './permissions-add-dialog/permissions-add-dialog.component'; import { UserAddDialogComponent } from './user-add-dialog/user-add-dialog.component'; import { UserDeleteDialogComponent } from './user-delete-dialog/user-delete-dialog.component'; import { UsersComponent } from './users.component'; -import { UsersService } from 'src/app/services/users.service'; -import { of } from 'rxjs'; -import { Angulartics2Module } from 'angulartics2'; -import { provideHttpClient } from '@angular/common/http'; -import { provideRouter } from '@angular/router'; describe('UsersComponent', () => { - let component: UsersComponent; - let fixture: ComponentFixture; - let usersService: UsersService; - let dialog: MatDialog; - const dialogRefSpyObj = { - afterClosed: vi.fn().mockReturnValue(of('delete')), - close: vi.fn(), - componentInstance: { deleteWidget: of('user_name') }, - }; - - const fakeGroup = { - "id": "a9a97cf1-cb2f-454b-a74e-0075dd07ad92", - "title": "Admin", - "isMain": true - }; - - beforeEach(async() => { - TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - MatDialogModule, - Angulartics2Module.forRoot(), - UsersComponent - ], - providers: [ - provideHttpClient(), - provideRouter([]), - { provide: MatDialogRef, useValue: {} }, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(UsersComponent); - component = fixture.componentInstance; - usersService = TestBed.inject(UsersService); - dialog = TestBed.inject(MatDialog); - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should permit action if access level is fullaccess', () => { - const isPermitted = component.isPermitted('fullaccess'); - expect(isPermitted).toBe(true); - }); - - it('should permit action if access level is edit', () => { - const isPermitted = component.isPermitted('edit'); - expect(isPermitted).toBe(true); - }); - - it('should not permit action if access level is none', () => { - const isPermitted = component.isPermitted('none'); - expect(isPermitted).toBe(false); - }); - - it('should set list of groups', () => { - const mockGroups = [ - { - "group": { - "id": "77154868-eaf0-4a53-9693-0470182d0971", - "title": "Sellers", - "isMain": false - }, - "accessLevel": "edit" - }, - { - "group": { - "id": "a9a97cf1-cb2f-454b-a74e-0075dd07ad92", - "title": "Admin", - "isMain": true - }, - "accessLevel": "edit" - } - ] - component.connectionID = '12345678'; - - vi.spyOn(usersService, 'fetchConnectionGroups').mockReturnValue(of(mockGroups)); - - component.getUsersGroups(); - expect(component.groups).toEqual(mockGroups); - }); - - it('should open create group dialog', () => { - const fakeCreateUsersGroupOpen = vi.spyOn(dialog, 'open'); - const event = { preventDefault: vi.fn(), stopImmediatePropagation: vi.fn() } as unknown as Event; - - component.openCreateUsersGroupDialog(event); - expect(fakeCreateUsersGroupOpen).toHaveBeenCalledWith(GroupAddDialogComponent, { - width: '25em' - }); - }); - - it('should open permissions dialog', () => { - const fakePermissionsDialogOpen = vi.spyOn(dialog, 'open').mockReturnValue(dialogRefSpyObj as any); - - component.openPermissionsDialog(fakeGroup); - expect(fakePermissionsDialogOpen).toHaveBeenCalledWith(PermissionsAddDialogComponent, { - width: '50em', - data: fakeGroup - }); - }); - - it('should open add user dialog', () => { - const fakeAddUserDialogOpen = vi.spyOn(dialog, 'open'); - - component.openAddUserDialog(fakeGroup); - expect(fakeAddUserDialogOpen).toHaveBeenCalledWith(UserAddDialogComponent, { - width: '25em', - data: { group: fakeGroup, availableMembers: []} - }); - }); - - it('should open delete group dialog', () => { - const fakeDeleteGroupDialogOpen = vi.spyOn(dialog, 'open'); - - component.openDeleteGroupDialog(fakeGroup); - expect(fakeDeleteGroupDialogOpen).toHaveBeenCalledWith(GroupDeleteDialogComponent, { - width: '25em', - data: fakeGroup - }); - }); - - it('should open delete user dialog', () => { - const fakeUser = { - id: 'user-12345678', - "createdAt": "2021-10-01T13:43:02.034Z", - "gclid": null, - "isActive": true, - "stripeId": "cus_123456789", - "email": "user@test.com" - } - - const fakeDeleteUserDialogOpen = vi.spyOn(dialog, 'open'); - - component.openDeleteUserDialog(fakeUser, fakeGroup); - expect(fakeDeleteUserDialogOpen).toHaveBeenCalledWith(UserDeleteDialogComponent, { - width: '25em', - data: {user: fakeUser, group: fakeGroup} - }); - }); - - it('should set users list of group in users object', async () => { - const mockGroupUsersList = [ - { - "id": "user-12345678", - "createdAt": "2021-11-17T16:07:13.955Z", - "gclid": null, - "isActive": true, - "stripeId": "cus_87654321", - "email": "user1@test.com" - }, - { - "id": "user-87654321", - "createdAt": "2021-10-01T13:43:02.034Z", - "gclid": null, - "isActive": true, - "stripeId": "cus_12345678", - "email": "user2@test.com" - } - ] - - vi.spyOn(usersService, 'fetcGroupUsers').mockReturnValue(of(mockGroupUsersList)); - - await component.fetchAndPopulateGroupUsers('12345678').toPromise(); - expect(component.users['12345678']).toEqual(mockGroupUsersList); - }); - - it('should set \'empty\' value in users object', async () => { - const mockGroupUsersList = [] - - vi.spyOn(usersService, 'fetcGroupUsers').mockReturnValue(of(mockGroupUsersList)); - - await component.fetchAndPopulateGroupUsers('12345678').toPromise(); - expect(component.users['12345678']).toEqual('empty'); - }); + let component: UsersComponent; + let fixture: ComponentFixture; + let usersService: UsersService; + let dialog: MatDialog; + const dialogRefSpyObj = { + afterClosed: vi.fn().mockReturnValue(of('delete')), + close: vi.fn(), + componentInstance: { deleteWidget: of('user_name') }, + }; + + const fakeGroup = { + id: 'a9a97cf1-cb2f-454b-a74e-0075dd07ad92', + title: 'Admin', + isMain: true, + }; + + beforeEach(async () => { + TestBed.configureTestingModule({ + imports: [MatSnackBarModule, MatDialogModule, Angulartics2Module.forRoot(), UsersComponent], + providers: [provideHttpClient(), provideRouter([]), { provide: MatDialogRef, useValue: {} }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UsersComponent); + component = fixture.componentInstance; + usersService = TestBed.inject(UsersService); + dialog = TestBed.inject(MatDialog); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should permit action if access level is fullaccess', () => { + const isPermitted = component.isPermitted('fullaccess'); + expect(isPermitted).toBe(true); + }); + + it('should permit action if access level is edit', () => { + const isPermitted = component.isPermitted('edit'); + expect(isPermitted).toBe(true); + }); + + it('should not permit action if access level is none', () => { + const isPermitted = component.isPermitted('none'); + expect(isPermitted).toBe(false); + }); + + it('should set list of groups', () => { + const mockGroups = [ + { + group: { + id: '77154868-eaf0-4a53-9693-0470182d0971', + title: 'Sellers', + isMain: false, + }, + accessLevel: 'edit', + }, + { + group: { + id: 'a9a97cf1-cb2f-454b-a74e-0075dd07ad92', + title: 'Admin', + isMain: true, + }, + accessLevel: 'edit', + }, + ]; + component.connectionID = '12345678'; + + vi.spyOn(usersService, 'fetchConnectionGroups').mockReturnValue(of(mockGroups)); + + component.getUsersGroups(); + expect(component.groups).toEqual(mockGroups); + }); + + it('should open create group dialog', () => { + const fakeCreateUsersGroupOpen = vi.spyOn(dialog, 'open'); + const event = { preventDefault: vi.fn(), stopImmediatePropagation: vi.fn() } as unknown as Event; + + component.openCreateUsersGroupDialog(event); + expect(fakeCreateUsersGroupOpen).toHaveBeenCalledWith(GroupAddDialogComponent, { + width: '25em', + }); + }); + + it('should open permissions dialog', () => { + const fakePermissionsDialogOpen = vi.spyOn(dialog, 'open').mockReturnValue(dialogRefSpyObj as any); + + component.openPermissionsDialog(fakeGroup); + expect(fakePermissionsDialogOpen).toHaveBeenCalledWith(PermissionsAddDialogComponent, { + width: '50em', + data: fakeGroup, + }); + }); + + it('should open add user dialog', () => { + const fakeAddUserDialogOpen = vi.spyOn(dialog, 'open'); + + component.openAddUserDialog(fakeGroup); + expect(fakeAddUserDialogOpen).toHaveBeenCalledWith(UserAddDialogComponent, { + width: '25em', + data: { group: fakeGroup, availableMembers: [] }, + }); + }); + + it('should open delete group dialog', () => { + const fakeDeleteGroupDialogOpen = vi.spyOn(dialog, 'open'); + + component.openDeleteGroupDialog(fakeGroup); + expect(fakeDeleteGroupDialogOpen).toHaveBeenCalledWith(GroupDeleteDialogComponent, { + width: '25em', + data: fakeGroup, + }); + }); + + it('should open delete user dialog', () => { + const fakeUser = { + id: 'user-12345678', + createdAt: '2021-10-01T13:43:02.034Z', + gclid: null, + isActive: true, + stripeId: 'cus_123456789', + email: 'user@test.com', + }; + + const fakeDeleteUserDialogOpen = vi.spyOn(dialog, 'open'); + + component.openDeleteUserDialog(fakeUser, fakeGroup); + expect(fakeDeleteUserDialogOpen).toHaveBeenCalledWith(UserDeleteDialogComponent, { + width: '25em', + data: { user: fakeUser, group: fakeGroup }, + }); + }); + + it('should set users list of group in users object', async () => { + const mockGroupUsersList = [ + { + id: 'user-12345678', + createdAt: '2021-11-17T16:07:13.955Z', + gclid: null, + isActive: true, + stripeId: 'cus_87654321', + email: 'user1@test.com', + }, + { + id: 'user-87654321', + createdAt: '2021-10-01T13:43:02.034Z', + gclid: null, + isActive: true, + stripeId: 'cus_12345678', + email: 'user2@test.com', + }, + ]; + + vi.spyOn(usersService, 'fetcGroupUsers').mockReturnValue(of(mockGroupUsersList)); + + await component.fetchAndPopulateGroupUsers('12345678').toPromise(); + expect(component.users['12345678']).toEqual(mockGroupUsersList); + }); + + it("should set 'empty' value in users object", async () => { + const mockGroupUsersList = []; + + vi.spyOn(usersService, 'fetcGroupUsers').mockReturnValue(of(mockGroupUsersList)); + + await component.fetchAndPopulateGroupUsers('12345678').toPromise(); + expect(component.users['12345678']).toEqual('empty'); + }); }); diff --git a/frontend/src/app/models/user.ts b/frontend/src/app/models/user.ts index 9ad3bae67..d64b74b77 100644 --- a/frontend/src/app/models/user.ts +++ b/frontend/src/app/models/user.ts @@ -2,99 +2,100 @@ import { CompanyMemberRole } from './company'; import { TablePermissions } from './table'; export interface NewAuthUser { - email: string, - password: string, + email: string; + password: string; + turnstileToken?: string; } export interface ExistingAuthUser { - email: string, - password: string, - companyId: string + email: string; + password: string; + companyId: string; } export interface UserGroup { - id: string, - title: string, - isMain: boolean, - users?: { - id: string, - isActive: boolean, - email: string, - createdAt?: string, - name: string, - is_2fa_enabled: boolean, - role: CompanyMemberRole - }[] + id: string; + title: string; + isMain: boolean; + users?: { + id: string; + isActive: boolean; + email: string; + createdAt?: string; + name: string; + is_2fa_enabled: boolean; + role: CompanyMemberRole; + }[]; } export interface UserGroupInfo { - group: UserGroup, - accessLevel: string + group: UserGroup; + accessLevel: string; } export interface GroupUser { - id: string, - createdAt: string, - gclid: string | null, - isActive: boolean, - stripeId: string, - email: string, + id: string; + createdAt: string; + gclid: string | null; + isActive: boolean; + stripeId: string; + email: string; } export enum SubscriptionPlans { - free = 'FREE_PLAN', - team = 'TEAM_PLAN', - enterprise = 'ENTERPRISE_PLAN', - teamAnnual = 'ANNUAL_TEAM_PLAN', - enterpriseAnnual = 'ANNUAL_ENTERPRISE_PLAN', + free = 'FREE_PLAN', + team = 'TEAM_PLAN', + enterprise = 'ENTERPRISE_PLAN', + teamAnnual = 'ANNUAL_TEAM_PLAN', + enterpriseAnnual = 'ANNUAL_ENTERPRISE_PLAN', } export enum RegistrationProvider { - Google = 'GOOGLE', - Github = 'GITHUB' + Google = 'GOOGLE', + Github = 'GITHUB', } export interface User { - id: string, - isActive: boolean, - email: string, - name?: string, - createdAt?: string, - portal_link: string, - subscriptionLevel: SubscriptionPlans, - is_2fa_enabled: boolean, - role: CompanyMemberRole, - externalRegistrationProvider: RegistrationProvider | null, - company: { - id: string, - } + id: string; + isActive: boolean; + email: string; + name?: string; + createdAt?: string; + portal_link: string; + subscriptionLevel: SubscriptionPlans; + is_2fa_enabled: boolean; + role: CompanyMemberRole; + externalRegistrationProvider: RegistrationProvider | null; + company: { + id: string; + }; } export enum AccessLevel { - None = 'none', - Readonly = 'readonly', - Edit = 'edit' + None = 'none', + Readonly = 'readonly', + Edit = 'edit', } export interface TablePermission { - tableName: string, - display_name: string, - accessLevel: TablePermissions + tableName: string; + display_name: string; + accessLevel: TablePermissions; } export interface Permissions { - connection: { - connectionId: string, - accessLevel: AccessLevel - }, - group: { - groupId: string, - accessLevel: AccessLevel - }, - tables: TablePermission[] + connection: { + connectionId: string; + accessLevel: AccessLevel; + }; + group: { + groupId: string; + accessLevel: AccessLevel; + }; + tables: TablePermission[]; } export interface ApiKey { - title: string, - id: string -} \ No newline at end of file + title: string; + id: string; +} diff --git a/frontend/src/app/services/auth.service.spec.ts b/frontend/src/app/services/auth.service.spec.ts index b8e4b9ed7..4080b86c5 100644 --- a/frontend/src/app/services/auth.service.spec.ts +++ b/frontend/src/app/services/auth.service.spec.ts @@ -1,305 +1,338 @@ -import { AlertActionType, AlertType } from '../models/alert'; +import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; - -import { AuthService } from './auth.service'; +import { TestBed } from '@angular/core/testing'; import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { AlertActionType, AlertType } from '../models/alert'; +import { AuthService } from './auth.service'; import { NotificationsService } from './notifications.service'; -import { TestBed } from '@angular/core/testing'; -import { provideHttpClient } from '@angular/common/http'; describe('AuthService', () => { - let service: AuthService; - let httpMock: HttpTestingController; - - let fakeNotifications; - - const fakeError = { - "message": "Auth error", - "statusCode": 400, - "originalMessage": "Auth error details" - } - - beforeEach(() => { - fakeNotifications = { - showErrorSnackbar: vi.fn(), - showSuccessSnackbar: vi.fn(), - showAlert: vi.fn() - }; - - TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule - ], - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - AuthService, - { - provide: NotificationsService, - useValue: fakeNotifications - } - ] - }); - - httpMock = TestBed.inject(HttpTestingController); - service = TestBed.inject(AuthService); - }); - - afterEach(() => { - httpMock.verify(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should call signUpUser', () => { - let isSignUpUserCalled = false; - - const userData = { - email: 'john@smith.com', - password: 'mM87654321' - }; - - const signUpResponse = { - expires: "2022-04-11T15:56:51.599Z" - } - - // @ts-expect-error - global.window.fbq = vi.fn(); - - service.signUpUser(userData).subscribe((res) => { - expect(res).toEqual(signUpResponse); - isSignUpUserCalled = true; - }); - - const req = httpMock.expectOne('/saas/user/register'); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual(userData); - req.flush(signUpResponse); - - expect(isSignUpUserCalled).toBe(true); - }); - - it('should fall for signUpUser and show Error alert', async () => { - const userData = { - email: 'john@smith.com', - password: 'mM87654321' - }; - - const tokenExpiration = service.signUpUser(userData).toPromise(); - - const req = httpMock.expectOne('/saas/user/register'); - expect(req.request.method).toBe("POST"); - req.flush(fakeError, {status: 400, statusText: ''}); - await tokenExpiration; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call loginUser', () => { - let isSignUpUserCalled = false; - - const userData = { - email: 'john@smith.com', - password: 'mM87654321', - companyId: 'company_1' - }; - - const loginResponse = { - expires: "2022-04-11T15:56:51.599Z" - } - - service.loginUser(userData).subscribe((res) => { - expect(res).toEqual(loginResponse); - isSignUpUserCalled = true; - }); - - const req = httpMock.expectOne(`/user/login`); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual(userData); - req.flush(loginResponse); - - expect(isSignUpUserCalled).toBe(true); - }); - - it('should fall for loginUser and show Error alert', async () => { - const userData = { - email: 'john@smith.com', - password: 'mM87654321', - companyId: 'company_1' - }; - - const tokenExpiration = service.loginUser(userData).toPromise(); - - const req = httpMock.expectOne(`/user/login`); - expect(req.request.method).toBe("POST"); - req.flush(fakeError, {status: 400, statusText: ''}); - await tokenExpiration; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call loginWith2FA', () => { - let isLoginWith2FACalled = false; - - const twofaResponse = {} - - service.loginWith2FA('123456').subscribe((res) => { - expect(res).toEqual(twofaResponse); - isLoginWith2FACalled = true; - }); - - const req = httpMock.expectOne(`/user/otp/login`); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual({ otpToken: '123456'}); - req.flush(twofaResponse); - - expect(isLoginWith2FACalled).toBe(true); - }); - - it('should fall for loginWith2FA and show Error alert', async () => { - const twofaResponse = service.loginWith2FA('123456').toPromise(); - - const req = httpMock.expectOne(`/user/otp/login`); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual({ otpToken: '123456'}); - req.flush(fakeError, {status: 400, statusText: ''}); - await twofaResponse; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call loginWithGoogle', () => { - let isLoginWithGoogleCalled = false; - - const googleResponse = {} - - service.loginWithGoogle('google-token-12345678').subscribe((res) => { - expect(res).toEqual(googleResponse); - isLoginWithGoogleCalled = true; - }); - - const req = httpMock.expectOne(`/saas/user/google/login`); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual({ token: 'google-token-12345678'}); - req.flush(googleResponse); - - expect(isLoginWithGoogleCalled).toBe(true); - }); - - it('should fall for loginWithGoogle and show Error alert', async () => { - const googleResponse = service.loginWithGoogle('google-token-12345678').toPromise(); - - const req = httpMock.expectOne(`/saas/user/google/login`); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual({ token: 'google-token-12345678'}); - req.flush(fakeError, {status: 400, statusText: ''}); - await googleResponse; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call requestEmailVerifications', () => { - let isRequestEmailVerificationsCalled = false; - - const googleResponse = {} - - service.requestEmailVerifications().subscribe((res) => { - expect(res).toEqual(googleResponse); - isRequestEmailVerificationsCalled = true; - }); - - const req = httpMock.expectOne(`/user/email/verify/request`); - expect(req.request.method).toBe("GET"); - req.flush(googleResponse); - - expect(isRequestEmailVerificationsCalled).toBe(true); - }); - - it('should fall for requestEmailVerifications and show Error alert', async () => { - const googleResponse = service.requestEmailVerifications().toPromise(); - - const req = httpMock.expectOne(`/user/email/verify/request`); - expect(req.request.method).toBe("GET"); - req.flush(fakeError, {status: 400, statusText: ''}); - await googleResponse; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call verifyEmail', () => { - let isSignUpUserCalled = false; - - const verifyResponse = { - message: "Email verified successfully" - } - - service.verifyEmail('12345678').subscribe((res) => { - expect(res).toEqual(verifyResponse); - isSignUpUserCalled = true; - }); - - const req = httpMock.expectOne(`/user/email/verify/12345678`); - expect(req.request.method).toBe("GET"); - req.flush(verifyResponse); - - expect(isSignUpUserCalled).toBe(true); - }); - - it('should fall for verifyEmail and show Error alert', async () => { - const verifyResponse = service.verifyEmail('12345678').toPromise(); - - const req = httpMock.expectOne(`/user/email/verify/12345678`); - expect(req.request.method).toBe("GET"); - req.flush(fakeError, {status: 400, statusText: ''}); - await verifyResponse; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call logOutUser', () => { - let isLogoutCalled = false; - - const logoutResponse = true - - service.logOutUser().subscribe(() => { - isLogoutCalled = true; - }); - - const req = httpMock.expectOne(`/user/logout`); - expect(req.request.method).toBe("POST"); - req.flush(logoutResponse); - - expect(isLogoutCalled).toBe(true); - }); - - it('should fall for logOutUser and show Error snackbar', async () => { - const logoutResponse = service.logOutUser().toPromise(); - - const req = httpMock.expectOne(`/user/logout`); - expect(req.request.method).toBe("POST"); - req.flush(fakeError, {status: 400, statusText: ''}); - await logoutResponse; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); + let service: AuthService; + let httpMock: HttpTestingController; + + let fakeNotifications; + + const fakeError = { + message: 'Auth error', + statusCode: 400, + originalMessage: 'Auth error details', + }; + + beforeEach(() => { + fakeNotifications = { + showErrorSnackbar: vi.fn(), + showSuccessSnackbar: vi.fn(), + showAlert: vi.fn(), + }; + + TestBed.configureTestingModule({ + imports: [MatSnackBarModule], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + AuthService, + { + provide: NotificationsService, + useValue: fakeNotifications, + }, + ], + }); + + httpMock = TestBed.inject(HttpTestingController); + service = TestBed.inject(AuthService); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call signUpUser', () => { + let isSignUpUserCalled = false; + + const userData = { + email: 'john@smith.com', + password: 'mM87654321', + }; + + const signUpResponse = { + expires: '2022-04-11T15:56:51.599Z', + }; + + // @ts-expect-error + global.window.fbq = vi.fn(); + + service.signUpUser(userData).subscribe((res) => { + expect(res).toEqual(signUpResponse); + isSignUpUserCalled = true; + }); + + const req = httpMock.expectOne('/saas/user/register'); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(userData); + req.flush(signUpResponse); + + expect(isSignUpUserCalled).toBe(true); + }); + + it('should fall for signUpUser and show Error alert', async () => { + const userData = { + email: 'john@smith.com', + password: 'mM87654321', + }; + + const tokenExpiration = service.signUpUser(userData).toPromise(); + + const req = httpMock.expectOne('/saas/user/register'); + expect(req.request.method).toBe('POST'); + req.flush(fakeError, { status: 400, statusText: '' }); + await tokenExpiration; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call loginUser', () => { + let isSignUpUserCalled = false; + + const userData = { + email: 'john@smith.com', + password: 'mM87654321', + companyId: 'company_1', + }; + + const loginResponse = { + expires: '2022-04-11T15:56:51.599Z', + }; + + service.loginUser(userData).subscribe((res) => { + expect(res).toEqual(loginResponse); + isSignUpUserCalled = true; + }); + + const req = httpMock.expectOne(`/user/login`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(userData); + req.flush(loginResponse); + + expect(isSignUpUserCalled).toBe(true); + }); + + it('should fall for loginUser and show Error alert', async () => { + const userData = { + email: 'john@smith.com', + password: 'mM87654321', + companyId: 'company_1', + }; + + const tokenExpiration = service.loginUser(userData).toPromise(); + + const req = httpMock.expectOne(`/user/login`); + expect(req.request.method).toBe('POST'); + req.flush(fakeError, { status: 400, statusText: '' }); + await tokenExpiration; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call loginWith2FA', () => { + let isLoginWith2FACalled = false; + + const twofaResponse = {}; + + service.loginWith2FA('123456').subscribe((res) => { + expect(res).toEqual(twofaResponse); + isLoginWith2FACalled = true; + }); + + const req = httpMock.expectOne(`/user/otp/login`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ otpToken: '123456' }); + req.flush(twofaResponse); + + expect(isLoginWith2FACalled).toBe(true); + }); + + it('should fall for loginWith2FA and show Error alert', async () => { + const twofaResponse = service.loginWith2FA('123456').toPromise(); + + const req = httpMock.expectOne(`/user/otp/login`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ otpToken: '123456' }); + req.flush(fakeError, { status: 400, statusText: '' }); + await twofaResponse; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call loginWithGoogle', () => { + let isLoginWithGoogleCalled = false; + + const googleResponse = {}; + + service.loginWithGoogle('google-token-12345678').subscribe((res) => { + expect(res).toEqual(googleResponse); + isLoginWithGoogleCalled = true; + }); + + const req = httpMock.expectOne(`/saas/user/google/login`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ token: 'google-token-12345678' }); + req.flush(googleResponse); + + expect(isLoginWithGoogleCalled).toBe(true); + }); + + it('should fall for loginWithGoogle and show Error alert', async () => { + const googleResponse = service.loginWithGoogle('google-token-12345678').toPromise(); + + const req = httpMock.expectOne(`/saas/user/google/login`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ token: 'google-token-12345678' }); + req.flush(fakeError, { status: 400, statusText: '' }); + await googleResponse; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call requestEmailVerifications', () => { + let isRequestEmailVerificationsCalled = false; + + const googleResponse = {}; + + service.requestEmailVerifications().subscribe((res) => { + expect(res).toEqual(googleResponse); + isRequestEmailVerificationsCalled = true; + }); + + const req = httpMock.expectOne(`/user/email/verify/request`); + expect(req.request.method).toBe('GET'); + req.flush(googleResponse); + + expect(isRequestEmailVerificationsCalled).toBe(true); + }); + + it('should fall for requestEmailVerifications and show Error alert', async () => { + const googleResponse = service.requestEmailVerifications().toPromise(); + + const req = httpMock.expectOne(`/user/email/verify/request`); + expect(req.request.method).toBe('GET'); + req.flush(fakeError, { status: 400, statusText: '' }); + await googleResponse; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call verifyEmail', () => { + let isSignUpUserCalled = false; + + const verifyResponse = { + message: 'Email verified successfully', + }; + + service.verifyEmail('12345678').subscribe((res) => { + expect(res).toEqual(verifyResponse); + isSignUpUserCalled = true; + }); + + const req = httpMock.expectOne(`/user/email/verify/12345678`); + expect(req.request.method).toBe('GET'); + req.flush(verifyResponse); + + expect(isSignUpUserCalled).toBe(true); + }); + + it('should fall for verifyEmail and show Error alert', async () => { + const verifyResponse = service.verifyEmail('12345678').toPromise(); + + const req = httpMock.expectOne(`/user/email/verify/12345678`); + expect(req.request.method).toBe('GET'); + req.flush(fakeError, { status: 400, statusText: '' }); + await verifyResponse; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call logOutUser', () => { + let isLogoutCalled = false; + + const logoutResponse = true; + + service.logOutUser().subscribe(() => { + isLogoutCalled = true; + }); + + const req = httpMock.expectOne(`/user/logout`); + expect(req.request.method).toBe('POST'); + req.flush(logoutResponse); + + expect(isLogoutCalled).toBe(true); + }); + + it('should fall for logOutUser and show Error snackbar', async () => { + const logoutResponse = service.logOutUser().toPromise(); + + const req = httpMock.expectOne(`/user/logout`); + expect(req.request.method).toBe('POST'); + req.flush(fakeError, { status: 400, statusText: '' }); + await logoutResponse; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); }); diff --git a/frontend/src/app/services/company.service.ts b/frontend/src/app/services/company.service.ts index 7c0581adf..9ecda2ece 100644 --- a/frontend/src/app/services/company.service.ts +++ b/frontend/src/app/services/company.service.ts @@ -110,12 +110,19 @@ export class CompanyService { ); } - inviteCompanyMember(companyId: string, groupId: string, email: string, role: CompanyMemberRole) { + inviteCompanyMember( + companyId: string, + groupId: string, + email: string, + role: CompanyMemberRole, + turnstileToken?: string, + ) { return this._http .put(`/company/user/${companyId}`, { groupId, email, role, + ...(turnstileToken ? { turnstileToken } : {}), }) .pipe( map(() => { diff --git a/frontend/src/app/services/master-password.service.spec.ts b/frontend/src/app/services/master-password.service.spec.ts index e3a5a139e..34a2077ec 100644 --- a/frontend/src/app/services/master-password.service.spec.ts +++ b/frontend/src/app/services/master-password.service.spec.ts @@ -1,8 +1,8 @@ +import { provideHttpClient } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { MasterPasswordDialogComponent } from '../components/master-password-dialog/master-password-dialog.component'; -import { provideHttpClient } from '@angular/common/http'; import { provideRouter } from '@angular/router'; +import { MasterPasswordDialogComponent } from '../components/master-password-dialog/master-password-dialog.component'; import { MasterPasswordService } from './master-password.service'; @@ -13,11 +13,7 @@ describe('MasterPasswordService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [MatDialogModule], - providers: [ - provideHttpClient(), - provideRouter([]), - { provide: MatDialogRef, useValue: { close: vi.fn() } } - ] + providers: [provideHttpClient(), provideRouter([]), { provide: MatDialogRef, useValue: { close: vi.fn() } }], }); service = TestBed.inject(MasterPasswordService); @@ -33,7 +29,9 @@ describe('MasterPasswordService', () => { }); it('should show Master password dialog', () => { - const fakeDialog = vi.spyOn(dialog, 'open').mockReturnValue({ afterClosed: () => ({ subscribe: () => {} }) } as any); + const fakeDialog = vi + .spyOn(dialog, 'open') + .mockReturnValue({ afterClosed: () => ({ subscribe: () => {} }) } as any); service.showMasterPasswordDialog(); expect(fakeDialog).toHaveBeenCalledWith(MasterPasswordDialogComponent, { width: '24em', diff --git a/frontend/src/app/services/notifications.service.spec.ts b/frontend/src/app/services/notifications.service.spec.ts index 491c7a3d8..3fbcd423f 100644 --- a/frontend/src/app/services/notifications.service.spec.ts +++ b/frontend/src/app/services/notifications.service.spec.ts @@ -1,103 +1,102 @@ -import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; - -import { NotificationsService } from './notifications.service'; import { TestBed } from '@angular/core/testing'; +import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { Alert, AlertActionType, AlertType } from '../models/alert'; +import { NotificationsService } from './notifications.service'; describe('NotificationsService', () => { - let service: NotificationsService; - let snackBar: MatSnackBar; + let service: NotificationsService; + let snackBar: MatSnackBar; - const alert: Alert = { - id: 0, - type: AlertType.Error, - message: 'Error message', - actions: [ - { - type: AlertActionType.Button, - caption: 'Dismiss' - } - ] - } + const alert: Alert = { + id: 0, + type: AlertType.Error, + message: 'Error message', + actions: [ + { + type: AlertActionType.Button, + caption: 'Dismiss', + }, + ], + }; - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ MatSnackBarModule ], - }) + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [MatSnackBarModule], + }); - service = TestBed.inject(NotificationsService); - snackBar = TestBed.inject(MatSnackBar); - service.idCounter = 0; - }); + service = TestBed.inject(NotificationsService); + snackBar = TestBed.inject(MatSnackBar); + service.idCounter = 0; + }); - it('should be created', () => { - expect(service).toBeTruthy(); - }); + it('should be created', () => { + expect(service).toBeTruthy(); + }); - it('should show ErrorSnackbar', () => { - const fakeSnackBar = vi.spyOn(snackBar, 'open'); - service.showErrorSnackbar('Error message.') - expect(fakeSnackBar).toHaveBeenCalledWith( - 'Error message.', - 'Dismiss', - Object({ - duration: 10000, - horizontalPosition: 'left' - }) - ); - }); + it('should show ErrorSnackbar', () => { + const fakeSnackBar = vi.spyOn(snackBar, 'open'); + service.showErrorSnackbar('Error message.'); + expect(fakeSnackBar).toHaveBeenCalledWith( + 'Error message.', + 'Dismiss', + Object({ + duration: 10000, + horizontalPosition: 'left', + }), + ); + }); - it('should show SuccessSnackbar', () => { - const fakeSnackBar = vi.spyOn(snackBar, 'open'); - service.showSuccessSnackbar('Success message.') - expect(fakeSnackBar).toHaveBeenCalledWith( - 'Success message.', - null, - Object({ - duration: 2500, - horizontalPosition: 'left' - }) - ); - }); + it('should show SuccessSnackbar', () => { + const fakeSnackBar = vi.spyOn(snackBar, 'open'); + service.showSuccessSnackbar('Success message.'); + expect(fakeSnackBar).toHaveBeenCalledWith( + 'Success message.', + null, + Object({ + duration: 2500, + horizontalPosition: 'left', + }), + ); + }); - it('should get alert', () => { - service.alert = alert; - expect(service.currentAlert).toEqual(alert); - }) + it('should get alert', () => { + service.alert = alert; + expect(service.currentAlert).toEqual(alert); + }); - it('should show new alert', () => { - service.alert = alert; - service.showAlert(AlertType.Error, 'Error message 2', [ - { - type: AlertActionType.Button, - caption: 'Dissmis' - } - ]); + it('should show new alert', () => { + service.alert = alert; + service.showAlert(AlertType.Error, 'Error message 2', [ + { + type: AlertActionType.Button, + caption: 'Dissmis', + }, + ]); - expect(service.alert).toEqual({ - id: 1, - type: AlertType.Error, - message: 'Error message 2', - actions: [ - { - type: AlertActionType.Button, - caption: 'Dissmis' - } - ] - }) - }); + expect(service.alert).toEqual({ + id: 1, + type: AlertType.Error, + message: 'Error message 2', + actions: [ + { + type: AlertActionType.Button, + caption: 'Dissmis', + }, + ], + }); + }); - it('should dissmis alert', () => { - service.alert = alert; - service.dismissAlert(); + it('should dissmis alert', () => { + service.alert = alert; + service.dismissAlert(); - expect(service.alert).toBeNull(); - }); + expect(service.alert).toBeNull(); + }); - it('should reset alert', () => { - service.alert = alert; - service.resetAlert(); + it('should reset alert', () => { + service.alert = alert; + service.resetAlert(); - expect(service.alert).toBeNull(); - }) + expect(service.alert).toBeNull(); + }); }); diff --git a/frontend/src/app/services/s3.service.spec.ts b/frontend/src/app/services/s3.service.spec.ts index 22cdf18fa..1da881f4e 100644 --- a/frontend/src/app/services/s3.service.spec.ts +++ b/frontend/src/app/services/s3.service.spec.ts @@ -1,40 +1,36 @@ -import { provideHttpClient } from "@angular/common/http"; -import { - HttpTestingController, - provideHttpClientTesting, -} from "@angular/common/http/testing"; -import { TestBed } from "@angular/core/testing"; -import { MatSnackBarModule } from "@angular/material/snack-bar"; -import { NotificationsService } from "./notifications.service"; -import { S3Service } from "./s3.service"; - -describe("S3Service", () => { +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { NotificationsService } from './notifications.service'; +import { S3Service } from './s3.service'; + +describe('S3Service', () => { let service: S3Service; let httpMock: HttpTestingController; let fakeNotifications: { showAlert: ReturnType; dismissAlert: ReturnType }; const mockFileUrlResponse = { - url: "https://s3.amazonaws.com/bucket/file.pdf?signature=abc123", - key: "prefix/file.pdf", + url: 'https://s3.amazonaws.com/bucket/file.pdf?signature=abc123', + key: 'prefix/file.pdf', expiresIn: 3600, }; const mockUploadUrlResponse = { - uploadUrl: - "https://s3.amazonaws.com/bucket/prefix/newfile.pdf?signature=xyz789", - key: "prefix/newfile.pdf", + uploadUrl: 'https://s3.amazonaws.com/bucket/prefix/newfile.pdf?signature=xyz789', + key: 'prefix/newfile.pdf', expiresIn: 3600, }; const fakeError = { - message: "Something went wrong", + message: 'Something went wrong', statusCode: 400, }; beforeEach(() => { fakeNotifications = { showAlert: vi.fn(), - dismissAlert: vi.fn() + dismissAlert: vi.fn(), }; TestBed.configureTestingModule({ @@ -55,153 +51,128 @@ describe("S3Service", () => { httpMock.verify(); }); - it("should be created", () => { + it('should be created', () => { expect(service).toBeTruthy(); }); - describe("getFileUrl", () => { - const connectionId = "conn-123"; - const tableName = "users"; - const fieldName = "avatar"; + describe('getFileUrl', () => { + const connectionId = 'conn-123'; + const tableName = 'users'; + const fieldName = 'avatar'; const rowPrimaryKey = { id: 1 }; - it("should fetch file URL successfully", () => { + it('should fetch file URL successfully', () => { let result: any; - service - .getFileUrl(connectionId, tableName, fieldName, rowPrimaryKey) - .subscribe((res) => { - result = res; - }); + service.getFileUrl(connectionId, tableName, fieldName, rowPrimaryKey).subscribe((res) => { + result = res; + }); const req = httpMock.expectOne( (request) => request.url === `/s3/file/${connectionId}` && - request.params.get("tableName") === tableName && - request.params.get("fieldName") === fieldName && - request.params.get("rowPrimaryKey") === JSON.stringify(rowPrimaryKey), + request.params.get('tableName') === tableName && + request.params.get('fieldName') === fieldName && + request.params.get('rowPrimaryKey') === JSON.stringify(rowPrimaryKey), ); - expect(req.request.method).toBe("GET"); + expect(req.request.method).toBe('GET'); req.flush(mockFileUrlResponse); expect(result).toEqual(mockFileUrlResponse); }); - it("should handle complex primary key", () => { - const complexPrimaryKey = { user_id: 1, org_id: "abc" }; + it('should handle complex primary key', () => { + const complexPrimaryKey = { user_id: 1, org_id: 'abc' }; let result: any; - service - .getFileUrl(connectionId, tableName, fieldName, complexPrimaryKey) - .subscribe((res) => { - result = res; - }); + service.getFileUrl(connectionId, tableName, fieldName, complexPrimaryKey).subscribe((res) => { + result = res; + }); const req = httpMock.expectOne( (request) => request.url === `/s3/file/${connectionId}` && - request.params.get("rowPrimaryKey") === - JSON.stringify(complexPrimaryKey), + request.params.get('rowPrimaryKey') === JSON.stringify(complexPrimaryKey), ); req.flush(mockFileUrlResponse); expect(result).toEqual(mockFileUrlResponse); }); - it("should show error alert on failure", async () => { - const promise = service - .getFileUrl(connectionId, tableName, fieldName, rowPrimaryKey) - .toPromise(); + it('should show error alert on failure', async () => { + const promise = service.getFileUrl(connectionId, tableName, fieldName, rowPrimaryKey).toPromise(); - const req = httpMock.expectOne( - (request) => request.url === `/s3/file/${connectionId}`, - ); - req.flush(fakeError, { status: 400, statusText: "Bad Request" }); + const req = httpMock.expectOne((request) => request.url === `/s3/file/${connectionId}`); + req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); await promise; expect(fakeNotifications.showAlert).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - abstract: "Failed to get S3 file URL", + abstract: 'Failed to get S3 file URL', details: fakeError.message, }), expect.any(Array), ); }); - it("should return EMPTY observable on error", async () => { + it('should return EMPTY observable on error', async () => { let emitted = false; const promise = new Promise((resolve) => { - service - .getFileUrl(connectionId, tableName, fieldName, rowPrimaryKey) - .subscribe({ - next: () => { - emitted = true; - }, - complete: () => { - resolve(); - }, - }); + service.getFileUrl(connectionId, tableName, fieldName, rowPrimaryKey).subscribe({ + next: () => { + emitted = true; + }, + complete: () => { + resolve(); + }, + }); }); - const req = httpMock.expectOne( - (request) => request.url === `/s3/file/${connectionId}`, - ); - req.flush(fakeError, { status: 400, statusText: "Bad Request" }); + const req = httpMock.expectOne((request) => request.url === `/s3/file/${connectionId}`); + req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); await promise; expect(emitted).toBe(false); }); }); - describe("getUploadUrl", () => { - const connectionId = "conn-123"; - const tableName = "users"; - const fieldName = "avatar"; - const filename = "document.pdf"; - const contentType = "application/pdf"; + describe('getUploadUrl', () => { + const connectionId = 'conn-123'; + const tableName = 'users'; + const fieldName = 'avatar'; + const filename = 'document.pdf'; + const contentType = 'application/pdf'; - it("should fetch upload URL successfully", () => { + it('should fetch upload URL successfully', () => { let result: any; - service - .getUploadUrl(connectionId, tableName, fieldName, filename, contentType) - .subscribe((res) => { - result = res; - }); + service.getUploadUrl(connectionId, tableName, fieldName, filename, contentType).subscribe((res) => { + result = res; + }); const req = httpMock.expectOne( (request) => request.url === `/s3/upload-url/${connectionId}` && - request.params.get("tableName") === tableName && - request.params.get("fieldName") === fieldName, + request.params.get('tableName') === tableName && + request.params.get('fieldName') === fieldName, ); - expect(req.request.method).toBe("POST"); + expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual({ filename, contentType }); req.flush(mockUploadUrlResponse); expect(result).toEqual(mockUploadUrlResponse); }); - it("should handle image upload", () => { - const imageFilename = "photo.jpg"; - const imageContentType = "image/jpeg"; + it('should handle image upload', () => { + const imageFilename = 'photo.jpg'; + const imageContentType = 'image/jpeg'; - service - .getUploadUrl( - connectionId, - tableName, - fieldName, - imageFilename, - imageContentType, - ) - .subscribe(); + service.getUploadUrl(connectionId, tableName, fieldName, imageFilename, imageContentType).subscribe(); - const req = httpMock.expectOne( - (request) => request.url === `/s3/upload-url/${connectionId}`, - ); + const req = httpMock.expectOne((request) => request.url === `/s3/upload-url/${connectionId}`); expect(req.request.body).toEqual({ filename: imageFilename, contentType: imageContentType, @@ -209,61 +180,52 @@ describe("S3Service", () => { req.flush(mockUploadUrlResponse); }); - it("should show error alert on failure", async () => { - const promise = service - .getUploadUrl(connectionId, tableName, fieldName, filename, contentType) - .toPromise(); + it('should show error alert on failure', async () => { + const promise = service.getUploadUrl(connectionId, tableName, fieldName, filename, contentType).toPromise(); - const req = httpMock.expectOne( - (request) => request.url === `/s3/upload-url/${connectionId}`, - ); - req.flush(fakeError, { status: 400, statusText: "Bad Request" }); + const req = httpMock.expectOne((request) => request.url === `/s3/upload-url/${connectionId}`); + req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); await promise; expect(fakeNotifications.showAlert).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - abstract: "Failed to get upload URL", + abstract: 'Failed to get upload URL', details: fakeError.message, }), expect.any(Array), ); }); - it("should return EMPTY observable on error", async () => { + it('should return EMPTY observable on error', async () => { let emitted = false; const promise = new Promise((resolve) => { - service - .getUploadUrl(connectionId, tableName, fieldName, filename, contentType) - .subscribe({ - next: () => { - emitted = true; - }, - complete: () => { - resolve(); - }, - }); + service.getUploadUrl(connectionId, tableName, fieldName, filename, contentType).subscribe({ + next: () => { + emitted = true; + }, + complete: () => { + resolve(); + }, + }); }); - const req = httpMock.expectOne( - (request) => request.url === `/s3/upload-url/${connectionId}`, - ); - req.flush(fakeError, { status: 400, statusText: "Bad Request" }); + const req = httpMock.expectOne((request) => request.url === `/s3/upload-url/${connectionId}`); + req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); await promise; expect(emitted).toBe(false); }); }); - describe("uploadToS3", () => { - const uploadUrl = - "https://s3.amazonaws.com/bucket/file.pdf?signature=abc123"; + describe('uploadToS3', () => { + const uploadUrl = 'https://s3.amazonaws.com/bucket/file.pdf?signature=abc123'; - it("should upload file to S3 successfully", () => { - const file = new File(["test content"], "test.pdf", { - type: "application/pdf", + it('should upload file to S3 successfully', () => { + const file = new File(['test content'], 'test.pdf', { + type: 'application/pdf', }); let completed = false; @@ -274,49 +236,49 @@ describe("S3Service", () => { }); const req = httpMock.expectOne(uploadUrl); - expect(req.request.method).toBe("PUT"); - expect(req.request.headers.get("Content-Type")).toBe("application/pdf"); + expect(req.request.method).toBe('PUT'); + expect(req.request.headers.get('Content-Type')).toBe('application/pdf'); expect(req.request.body).toBe(file); req.flush(null); expect(completed).toBe(true); }); - it("should upload image file with correct content type", () => { - const file = new File(["image data"], "photo.jpg", { - type: "image/jpeg", + it('should upload image file with correct content type', () => { + const file = new File(['image data'], 'photo.jpg', { + type: 'image/jpeg', }); service.uploadToS3(uploadUrl, file).subscribe(); const req = httpMock.expectOne(uploadUrl); - expect(req.request.headers.get("Content-Type")).toBe("image/jpeg"); + expect(req.request.headers.get('Content-Type')).toBe('image/jpeg'); req.flush(null); }); - it("should show error alert on upload failure", async () => { - const file = new File(["test content"], "test.pdf", { - type: "application/pdf", + it('should show error alert on upload failure', async () => { + const file = new File(['test content'], 'test.pdf', { + type: 'application/pdf', }); const promise = service.uploadToS3(uploadUrl, file).toPromise(); const req = httpMock.expectOne(uploadUrl); - req.flush(null, { status: 500, statusText: "Internal Server Error" }); + req.flush(null, { status: 500, statusText: 'Internal Server Error' }); await promise; expect(fakeNotifications.showAlert).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - abstract: "File upload failed", + abstract: 'File upload failed', }), expect.any(Array), ); }); - it("should return EMPTY observable on error", async () => { - const file = new File(["test content"], "test.pdf", { - type: "application/pdf", + it('should return EMPTY observable on error', async () => { + const file = new File(['test content'], 'test.pdf', { + type: 'application/pdf', }); let emitted = false; @@ -332,7 +294,7 @@ describe("S3Service", () => { }); const req = httpMock.expectOne(uploadUrl); - req.flush(null, { status: 500, statusText: "Internal Server Error" }); + req.flush(null, { status: 500, statusText: 'Internal Server Error' }); await promise; expect(emitted).toBe(false); diff --git a/frontend/src/app/services/secrets.service.spec.ts b/frontend/src/app/services/secrets.service.spec.ts index b541dc2f5..e60f9046e 100644 --- a/frontend/src/app/services/secrets.service.spec.ts +++ b/frontend/src/app/services/secrets.service.spec.ts @@ -1,522 +1,517 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; import { MatSnackBarModule } from '@angular/material/snack-bar'; - -import { SecretsService } from './secrets.service'; -import { NotificationsService } from './notifications.service'; import { - Secret, - SecretListResponse, - AuditLogResponse, - CreateSecretPayload, - UpdateSecretPayload, - DeleteSecretResponse, + AuditLogResponse, + CreateSecretPayload, + DeleteSecretResponse, + Secret, + SecretListResponse, + UpdateSecretPayload, } from '../models/secret'; +import { NotificationsService } from './notifications.service'; +import { SecretsService } from './secrets.service'; describe('SecretsService', () => { - let service: SecretsService; - let httpMock: HttpTestingController; - let fakeNotifications: { showErrorSnackbar: ReturnType; showSuccessSnackbar: ReturnType }; - - const mockSecret: Secret = { - id: '1', - slug: 'test-secret', - companyId: 'company-1', - createdAt: '2024-01-01T00:00:00Z', - updatedAt: '2024-01-01T00:00:00Z', - masterEncryption: false, - }; - - const mockSecretWithExpiration: Secret = { - ...mockSecret, - expiresAt: '2025-01-01T00:00:00Z', - }; - - const mockSecretListResponse: SecretListResponse = { - data: [mockSecret, mockSecretWithExpiration], - pagination: { - total: 2, - currentPage: 1, - perPage: 20, - lastPage: 1, - }, - }; - - const mockAuditLogResponse: AuditLogResponse = { - data: [ - { - id: '1', - action: 'create', - user: { id: 'user-1', email: 'user@example.com' }, - accessedAt: '2024-01-01T00:00:00Z', - success: true, - }, - { - id: '2', - action: 'view', - user: { id: 'user-1', email: 'user@example.com' }, - accessedAt: '2024-01-02T00:00:00Z', - success: true, - }, - ], - pagination: { - total: 2, - currentPage: 1, - perPage: 50, - lastPage: 1, - }, - }; - - const mockDeleteResponse: DeleteSecretResponse = { - message: 'Secret deleted successfully', - deletedAt: '2024-01-01T00:00:00Z', - }; - - const fakeError = { - message: 'Something went wrong', - statusCode: 400, - }; - - beforeEach(() => { - fakeNotifications = { - showErrorSnackbar: vi.fn(), - showSuccessSnackbar: vi.fn() - }; - - TestBed.configureTestingModule({ - imports: [MatSnackBarModule], - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - SecretsService, - { provide: NotificationsService, useValue: fakeNotifications }, - ], - }); - - service = TestBed.inject(SecretsService); - httpMock = TestBed.inject(HttpTestingController); - }); - - afterEach(() => { - httpMock.verify(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - describe('fetchSecrets', () => { - it('should fetch secrets with default pagination', () => { - let result: SecretListResponse | undefined; - - service.fetchSecrets().subscribe((res) => { - result = res; - }); - - const req = httpMock.expectOne('/secrets?page=1&limit=20'); - expect(req.request.method).toBe('GET'); - req.flush(mockSecretListResponse); - - expect(result).toEqual(mockSecretListResponse); - }); - - it('should fetch secrets with custom pagination', () => { - let result: SecretListResponse | undefined; - - service.fetchSecrets(2, 10).subscribe((res) => { - result = res; - }); - - const req = httpMock.expectOne('/secrets?page=2&limit=10'); - expect(req.request.method).toBe('GET'); - req.flush(mockSecretListResponse); - - expect(result).toEqual(mockSecretListResponse); - }); - - it('should fetch secrets with search query', () => { - let result: SecretListResponse | undefined; - - service.fetchSecrets(1, 20, 'api-key').subscribe((res) => { - result = res; - }); - - const req = httpMock.expectOne('/secrets?page=1&limit=20&search=api-key'); - expect(req.request.method).toBe('GET'); - req.flush(mockSecretListResponse); - - expect(result).toEqual(mockSecretListResponse); - }); - - it('should show error snackbar on fetch failure', async () => { - const promise = service.fetchSecrets().toPromise(); - - const req = httpMock.expectOne('/secrets?page=1&limit=20'); - req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); - - await promise; + let service: SecretsService; + let httpMock: HttpTestingController; + let fakeNotifications: { showErrorSnackbar: ReturnType; showSuccessSnackbar: ReturnType }; + + const mockSecret: Secret = { + id: '1', + slug: 'test-secret', + companyId: 'company-1', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + masterEncryption: false, + }; + + const mockSecretWithExpiration: Secret = { + ...mockSecret, + expiresAt: '2025-01-01T00:00:00Z', + }; + + const mockSecretListResponse: SecretListResponse = { + data: [mockSecret, mockSecretWithExpiration], + pagination: { + total: 2, + currentPage: 1, + perPage: 20, + lastPage: 1, + }, + }; + + const mockAuditLogResponse: AuditLogResponse = { + data: [ + { + id: '1', + action: 'create', + user: { id: 'user-1', email: 'user@example.com' }, + accessedAt: '2024-01-01T00:00:00Z', + success: true, + }, + { + id: '2', + action: 'view', + user: { id: 'user-1', email: 'user@example.com' }, + accessedAt: '2024-01-02T00:00:00Z', + success: true, + }, + ], + pagination: { + total: 2, + currentPage: 1, + perPage: 50, + lastPage: 1, + }, + }; + + const mockDeleteResponse: DeleteSecretResponse = { + message: 'Secret deleted successfully', + deletedAt: '2024-01-01T00:00:00Z', + }; + + const fakeError = { + message: 'Something went wrong', + statusCode: 400, + }; + + beforeEach(() => { + fakeNotifications = { + showErrorSnackbar: vi.fn(), + showSuccessSnackbar: vi.fn(), + }; + + TestBed.configureTestingModule({ + imports: [MatSnackBarModule], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + SecretsService, + { provide: NotificationsService, useValue: fakeNotifications }, + ], + }); + + service = TestBed.inject(SecretsService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('fetchSecrets', () => { + it('should fetch secrets with default pagination', () => { + let result: SecretListResponse | undefined; + + service.fetchSecrets().subscribe((res) => { + result = res; + }); + + const req = httpMock.expectOne('/secrets?page=1&limit=20'); + expect(req.request.method).toBe('GET'); + req.flush(mockSecretListResponse); + + expect(result).toEqual(mockSecretListResponse); + }); + + it('should fetch secrets with custom pagination', () => { + let result: SecretListResponse | undefined; + + service.fetchSecrets(2, 10).subscribe((res) => { + result = res; + }); + + const req = httpMock.expectOne('/secrets?page=2&limit=10'); + expect(req.request.method).toBe('GET'); + req.flush(mockSecretListResponse); + + expect(result).toEqual(mockSecretListResponse); + }); + + it('should fetch secrets with search query', () => { + let result: SecretListResponse | undefined; + + service.fetchSecrets(1, 20, 'api-key').subscribe((res) => { + result = res; + }); + + const req = httpMock.expectOne('/secrets?page=1&limit=20&search=api-key'); + expect(req.request.method).toBe('GET'); + req.flush(mockSecretListResponse); + + expect(result).toEqual(mockSecretListResponse); + }); + + it('should show error snackbar on fetch failure', async () => { + const promise = service.fetchSecrets().toPromise(); + + const req = httpMock.expectOne('/secrets?page=1&limit=20'); + req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); + + await promise; - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); - it('should show default error message when error has no message', async () => { - const promise = service.fetchSecrets().toPromise(); + it('should show default error message when error has no message', async () => { + const promise = service.fetchSecrets().toPromise(); - const req = httpMock.expectOne('/secrets?page=1&limit=20'); - req.flush({}, { status: 500, statusText: 'Internal Server Error' }); + const req = httpMock.expectOne('/secrets?page=1&limit=20'); + req.flush({}, { status: 500, statusText: 'Internal Server Error' }); - await promise; + await promise; - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith('Failed to fetch secrets'); - }); - }); + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith('Failed to fetch secrets'); + }); + }); - describe('createSecret', () => { - const createPayload: CreateSecretPayload = { - slug: 'new-secret', - value: 'secret-value', - }; + describe('createSecret', () => { + const createPayload: CreateSecretPayload = { + slug: 'new-secret', + value: 'secret-value', + }; - it('should create a secret successfully', () => { - let result: Secret | undefined; + it('should create a secret successfully', () => { + let result: Secret | undefined; - service.createSecret(createPayload).subscribe((res) => { - result = res; - }); + service.createSecret(createPayload).subscribe((res) => { + result = res; + }); - const req = httpMock.expectOne('/secrets'); - expect(req.request.method).toBe('POST'); - expect(req.request.body).toEqual(createPayload); - req.flush(mockSecret); + const req = httpMock.expectOne('/secrets'); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(createPayload); + req.flush(mockSecret); - expect(result).toEqual(mockSecret); - expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Secret created successfully'); - }); + expect(result).toEqual(mockSecret); + expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Secret created successfully'); + }); - it('should create a secret with expiration', () => { - const payloadWithExpiration: CreateSecretPayload = { - ...createPayload, - expiresAt: '2025-01-01T00:00:00Z', - }; + it('should create a secret with expiration', () => { + const payloadWithExpiration: CreateSecretPayload = { + ...createPayload, + expiresAt: '2025-01-01T00:00:00Z', + }; - service.createSecret(payloadWithExpiration).subscribe(); + service.createSecret(payloadWithExpiration).subscribe(); - const req = httpMock.expectOne('/secrets'); - expect(req.request.body).toEqual(payloadWithExpiration); - req.flush(mockSecretWithExpiration); - }); + const req = httpMock.expectOne('/secrets'); + expect(req.request.body).toEqual(payloadWithExpiration); + req.flush(mockSecretWithExpiration); + }); - it('should create a secret with master encryption', () => { - const payloadWithEncryption: CreateSecretPayload = { - ...createPayload, - masterEncryption: true, - masterPassword: 'my-master-password', - }; + it('should create a secret with master encryption', () => { + const payloadWithEncryption: CreateSecretPayload = { + ...createPayload, + masterEncryption: true, + masterPassword: 'my-master-password', + }; - service.createSecret(payloadWithEncryption).subscribe(); + service.createSecret(payloadWithEncryption).subscribe(); - const req = httpMock.expectOne('/secrets'); - expect(req.request.body).toEqual(payloadWithEncryption); - req.flush({ ...mockSecret, masterEncryption: true }); - }); + const req = httpMock.expectOne('/secrets'); + expect(req.request.body).toEqual(payloadWithEncryption); + req.flush({ ...mockSecret, masterEncryption: true }); + }); - it('should emit secretsUpdated on successful creation', () => { - let updateAction: string | undefined; - service.cast.subscribe((action) => { - updateAction = action; - }); + it('should emit secretsUpdated on successful creation', () => { + let updateAction: string | undefined; + service.cast.subscribe((action) => { + updateAction = action; + }); - service.createSecret(createPayload).subscribe(); + service.createSecret(createPayload).subscribe(); - const req = httpMock.expectOne('/secrets'); - req.flush(mockSecret); + const req = httpMock.expectOne('/secrets'); + req.flush(mockSecret); - expect(updateAction).toBe('created'); - }); + expect(updateAction).toBe('created'); + }); - it('should show conflict error when slug already exists', async () => { - const promise = service.createSecret(createPayload).toPromise(); + it('should show conflict error when slug already exists', async () => { + const promise = service.createSecret(createPayload).toPromise(); - const req = httpMock.expectOne('/secrets'); - req.flush({ message: 'Conflict' }, { status: 409, statusText: 'Conflict' }); + const req = httpMock.expectOne('/secrets'); + req.flush({ message: 'Conflict' }, { status: 409, statusText: 'Conflict' }); - await promise; + await promise; - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith( - 'A secret with this slug already exists' - ); - }); + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith('A secret with this slug already exists'); + }); - it('should show generic error on other failures', async () => { - const promise = service.createSecret(createPayload).toPromise(); + it('should show generic error on other failures', async () => { + const promise = service.createSecret(createPayload).toPromise(); - const req = httpMock.expectOne('/secrets'); - req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); + const req = httpMock.expectOne('/secrets'); + req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); - await promise; + await promise; - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - }); + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + }); - describe('updateSecret', () => { - const updatePayload: UpdateSecretPayload = { - value: 'new-value', - }; + describe('updateSecret', () => { + const updatePayload: UpdateSecretPayload = { + value: 'new-value', + }; - it('should update a secret successfully', () => { - let result: Secret | undefined; + it('should update a secret successfully', () => { + let result: Secret | undefined; - service.updateSecret('test-secret', updatePayload).subscribe((res) => { - result = res; - }); + service.updateSecret('test-secret', updatePayload).subscribe((res) => { + result = res; + }); - const req = httpMock.expectOne('/secrets/test-secret'); - expect(req.request.method).toBe('PUT'); - expect(req.request.body).toEqual(updatePayload); - req.flush(mockSecret); + const req = httpMock.expectOne('/secrets/test-secret'); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual(updatePayload); + req.flush(mockSecret); - expect(result).toEqual(mockSecret); - expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Secret updated successfully'); - }); + expect(result).toEqual(mockSecret); + expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Secret updated successfully'); + }); - it('should update a secret with new expiration', () => { - const payloadWithExpiration: UpdateSecretPayload = { - ...updatePayload, - expiresAt: '2026-01-01T00:00:00Z', - }; + it('should update a secret with new expiration', () => { + const payloadWithExpiration: UpdateSecretPayload = { + ...updatePayload, + expiresAt: '2026-01-01T00:00:00Z', + }; - service.updateSecret('test-secret', payloadWithExpiration).subscribe(); + service.updateSecret('test-secret', payloadWithExpiration).subscribe(); - const req = httpMock.expectOne('/secrets/test-secret'); - expect(req.request.body).toEqual(payloadWithExpiration); - req.flush(mockSecretWithExpiration); - }); + const req = httpMock.expectOne('/secrets/test-secret'); + expect(req.request.body).toEqual(payloadWithExpiration); + req.flush(mockSecretWithExpiration); + }); - it('should clear expiration when expiresAt is null', () => { - const payloadClearExpiration: UpdateSecretPayload = { - ...updatePayload, - expiresAt: null, - }; + it('should clear expiration when expiresAt is null', () => { + const payloadClearExpiration: UpdateSecretPayload = { + ...updatePayload, + expiresAt: null, + }; - service.updateSecret('test-secret', payloadClearExpiration).subscribe(); + service.updateSecret('test-secret', payloadClearExpiration).subscribe(); - const req = httpMock.expectOne('/secrets/test-secret'); - expect(req.request.body).toEqual(payloadClearExpiration); - req.flush(mockSecret); - }); + const req = httpMock.expectOne('/secrets/test-secret'); + expect(req.request.body).toEqual(payloadClearExpiration); + req.flush(mockSecret); + }); - it('should send master password in header when provided', () => { - service.updateSecret('test-secret', updatePayload, 'master-password-123').subscribe(); + it('should send master password in header when provided', () => { + service.updateSecret('test-secret', updatePayload, 'master-password-123').subscribe(); - const req = httpMock.expectOne('/secrets/test-secret'); - expect(req.request.headers.get('masterpwd')).toBe('master-password-123'); - req.flush(mockSecret); - }); + const req = httpMock.expectOne('/secrets/test-secret'); + expect(req.request.headers.get('masterpwd')).toBe('master-password-123'); + req.flush(mockSecret); + }); - it('should not send master password header when not provided', () => { - service.updateSecret('test-secret', updatePayload).subscribe(); + it('should not send master password header when not provided', () => { + service.updateSecret('test-secret', updatePayload).subscribe(); - const req = httpMock.expectOne('/secrets/test-secret'); - expect(req.request.headers.has('masterpwd')).toBe(false); - req.flush(mockSecret); - }); + const req = httpMock.expectOne('/secrets/test-secret'); + expect(req.request.headers.has('masterpwd')).toBe(false); + req.flush(mockSecret); + }); - it('should emit secretsUpdated on successful update', () => { - let updateAction: string | undefined; - service.cast.subscribe((action) => { - updateAction = action; - }); + it('should emit secretsUpdated on successful update', () => { + let updateAction: string | undefined; + service.cast.subscribe((action) => { + updateAction = action; + }); - service.updateSecret('test-secret', updatePayload).subscribe(); + service.updateSecret('test-secret', updatePayload).subscribe(); - const req = httpMock.expectOne('/secrets/test-secret'); - req.flush(mockSecret); + const req = httpMock.expectOne('/secrets/test-secret'); + req.flush(mockSecret); - expect(updateAction).toBe('updated'); - }); + expect(updateAction).toBe('updated'); + }); - it('should throw error on 403 (invalid master password)', async () => { - let errorThrown = false; + it('should throw error on 403 (invalid master password)', async () => { + let errorThrown = false; - service.updateSecret('test-secret', updatePayload, 'wrong-password').subscribe({ - error: (err) => { - errorThrown = true; - expect(err.status).toBe(403); - }, - }); + service.updateSecret('test-secret', updatePayload, 'wrong-password').subscribe({ + error: (err) => { + errorThrown = true; + expect(err.status).toBe(403); + }, + }); - const req = httpMock.expectOne('/secrets/test-secret'); - req.flush({ message: 'Invalid master password' }, { status: 403, statusText: 'Forbidden' }); + const req = httpMock.expectOne('/secrets/test-secret'); + req.flush({ message: 'Invalid master password' }, { status: 403, statusText: 'Forbidden' }); - expect(errorThrown).toBe(true); - }); + expect(errorThrown).toBe(true); + }); - it('should show error for expired secret (410)', async () => { - const promise = service.updateSecret('test-secret', updatePayload).toPromise(); + it('should show error for expired secret (410)', async () => { + const promise = service.updateSecret('test-secret', updatePayload).toPromise(); - const req = httpMock.expectOne('/secrets/test-secret'); - req.flush({ message: 'Secret expired' }, { status: 410, statusText: 'Gone' }); + const req = httpMock.expectOne('/secrets/test-secret'); + req.flush({ message: 'Secret expired' }, { status: 410, statusText: 'Gone' }); - await promise; + await promise; - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith( - 'Cannot update an expired secret' - ); - }); + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith('Cannot update an expired secret'); + }); - it('should show generic error on other failures', async () => { - const promise = service.updateSecret('test-secret', updatePayload).toPromise(); + it('should show generic error on other failures', async () => { + const promise = service.updateSecret('test-secret', updatePayload).toPromise(); - const req = httpMock.expectOne('/secrets/test-secret'); - req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); + const req = httpMock.expectOne('/secrets/test-secret'); + req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); - await promise; + await promise; - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - }); + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + }); - describe('deleteSecret', () => { - it('should delete a secret successfully', () => { - let result: DeleteSecretResponse | undefined; + describe('deleteSecret', () => { + it('should delete a secret successfully', () => { + let result: DeleteSecretResponse | undefined; - service.deleteSecret('test-secret').subscribe((res) => { - result = res; - }); + service.deleteSecret('test-secret').subscribe((res) => { + result = res; + }); - const req = httpMock.expectOne('/secrets/test-secret'); - expect(req.request.method).toBe('DELETE'); - req.flush(mockDeleteResponse); + const req = httpMock.expectOne('/secrets/test-secret'); + expect(req.request.method).toBe('DELETE'); + req.flush(mockDeleteResponse); - expect(result).toEqual(mockDeleteResponse); - expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Secret deleted successfully'); - }); + expect(result).toEqual(mockDeleteResponse); + expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Secret deleted successfully'); + }); - it('should emit secretsUpdated on successful deletion', () => { - let updateAction: string | undefined; - service.cast.subscribe((action) => { - updateAction = action; - }); + it('should emit secretsUpdated on successful deletion', () => { + let updateAction: string | undefined; + service.cast.subscribe((action) => { + updateAction = action; + }); - service.deleteSecret('test-secret').subscribe(); + service.deleteSecret('test-secret').subscribe(); - const req = httpMock.expectOne('/secrets/test-secret'); - req.flush(mockDeleteResponse); + const req = httpMock.expectOne('/secrets/test-secret'); + req.flush(mockDeleteResponse); - expect(updateAction).toBe('deleted'); - }); + expect(updateAction).toBe('deleted'); + }); - it('should show error on delete failure', async () => { - const promise = service.deleteSecret('test-secret').toPromise(); + it('should show error on delete failure', async () => { + const promise = service.deleteSecret('test-secret').toPromise(); - const req = httpMock.expectOne('/secrets/test-secret'); - req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); + const req = httpMock.expectOne('/secrets/test-secret'); + req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); - await promise; + await promise; - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); - it('should show default error message when error has no message', async () => { - const promise = service.deleteSecret('test-secret').toPromise(); + it('should show default error message when error has no message', async () => { + const promise = service.deleteSecret('test-secret').toPromise(); - const req = httpMock.expectOne('/secrets/test-secret'); - req.flush({}, { status: 500, statusText: 'Internal Server Error' }); + const req = httpMock.expectOne('/secrets/test-secret'); + req.flush({}, { status: 500, statusText: 'Internal Server Error' }); - await promise; + await promise; - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith('Failed to delete secret'); - }); - }); + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith('Failed to delete secret'); + }); + }); - describe('getAuditLog', () => { - it('should fetch audit log with default pagination', () => { - let result: AuditLogResponse | undefined; + describe('getAuditLog', () => { + it('should fetch audit log with default pagination', () => { + let result: AuditLogResponse | undefined; - service.getAuditLog('test-secret').subscribe((res) => { - result = res; - }); + service.getAuditLog('test-secret').subscribe((res) => { + result = res; + }); - const req = httpMock.expectOne('/secrets/test-secret/audit-log?page=1&limit=50'); - expect(req.request.method).toBe('GET'); - req.flush(mockAuditLogResponse); + const req = httpMock.expectOne('/secrets/test-secret/audit-log?page=1&limit=50'); + expect(req.request.method).toBe('GET'); + req.flush(mockAuditLogResponse); - expect(result).toEqual(mockAuditLogResponse); - }); + expect(result).toEqual(mockAuditLogResponse); + }); - it('should fetch audit log with custom pagination', () => { - let result: AuditLogResponse | undefined; + it('should fetch audit log with custom pagination', () => { + let result: AuditLogResponse | undefined; - service.getAuditLog('test-secret', 2, 25).subscribe((res) => { - result = res; - }); + service.getAuditLog('test-secret', 2, 25).subscribe((res) => { + result = res; + }); - const req = httpMock.expectOne('/secrets/test-secret/audit-log?page=2&limit=25'); - expect(req.request.method).toBe('GET'); - req.flush(mockAuditLogResponse); + const req = httpMock.expectOne('/secrets/test-secret/audit-log?page=2&limit=25'); + expect(req.request.method).toBe('GET'); + req.flush(mockAuditLogResponse); - expect(result).toEqual(mockAuditLogResponse); - }); + expect(result).toEqual(mockAuditLogResponse); + }); - it('should show error on audit log fetch failure', async () => { - const promise = service.getAuditLog('test-secret').toPromise(); + it('should show error on audit log fetch failure', async () => { + const promise = service.getAuditLog('test-secret').toPromise(); - const req = httpMock.expectOne('/secrets/test-secret/audit-log?page=1&limit=50'); - req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); + const req = httpMock.expectOne('/secrets/test-secret/audit-log?page=1&limit=50'); + req.flush(fakeError, { status: 400, statusText: 'Bad Request' }); - await promise; + await promise; - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); - it('should show default error message when error has no message', async () => { - const promise = service.getAuditLog('test-secret').toPromise(); + it('should show default error message when error has no message', async () => { + const promise = service.getAuditLog('test-secret').toPromise(); - const req = httpMock.expectOne('/secrets/test-secret/audit-log?page=1&limit=50'); - req.flush({}, { status: 500, statusText: 'Internal Server Error' }); + const req = httpMock.expectOne('/secrets/test-secret/audit-log?page=1&limit=50'); + req.flush({}, { status: 500, statusText: 'Internal Server Error' }); - await promise; + await promise; - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith('Failed to fetch audit log'); - }); - }); + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith('Failed to fetch audit log'); + }); + }); - describe('cast observable', () => { - it('should initially emit empty string', () => { - let emittedValue: string | undefined; + describe('cast observable', () => { + it('should initially emit empty string', () => { + let emittedValue: string | undefined; - service.cast.subscribe((value) => { - emittedValue = value; - }); + service.cast.subscribe((value) => { + emittedValue = value; + }); - expect(emittedValue).toBe(''); - }); + expect(emittedValue).toBe(''); + }); - it('should emit actions when secrets are modified', () => { - const emittedValues: string[] = []; + it('should emit actions when secrets are modified', () => { + const emittedValues: string[] = []; - service.cast.subscribe((value) => { - emittedValues.push(value); - }); + service.cast.subscribe((value) => { + emittedValues.push(value); + }); - // Create a secret - service.createSecret({ slug: 'test', value: 'value' }).subscribe(); - const createReq = httpMock.expectOne('/secrets'); - createReq.flush(mockSecret); + // Create a secret + service.createSecret({ slug: 'test', value: 'value' }).subscribe(); + const createReq = httpMock.expectOne('/secrets'); + createReq.flush(mockSecret); - // Update a secret - service.updateSecret('test', { value: 'new-value' }).subscribe(); - const updateReq = httpMock.expectOne('/secrets/test'); - updateReq.flush(mockSecret); + // Update a secret + service.updateSecret('test', { value: 'new-value' }).subscribe(); + const updateReq = httpMock.expectOne('/secrets/test'); + updateReq.flush(mockSecret); - // Delete a secret - service.deleteSecret('test').subscribe(); - const deleteReq = httpMock.expectOne('/secrets/test'); - deleteReq.flush(mockDeleteResponse); + // Delete a secret + service.deleteSecret('test').subscribe(); + const deleteReq = httpMock.expectOne('/secrets/test'); + deleteReq.flush(mockDeleteResponse); - expect(emittedValues).toEqual(['', 'created', 'updated', 'deleted']); - }); - }); + expect(emittedValues).toEqual(['', 'created', 'updated', 'deleted']); + }); + }); }); diff --git a/frontend/src/app/services/table-row.service.spec.ts b/frontend/src/app/services/table-row.service.spec.ts index 7b4e25cdc..de63002c4 100644 --- a/frontend/src/app/services/table-row.service.spec.ts +++ b/frontend/src/app/services/table-row.service.spec.ts @@ -1,194 +1,212 @@ -import { TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { TableRowService } from './table-row.service'; -import { NotificationsService } from './notifications.service'; import { AlertActionType, AlertType } from '../models/alert'; -import { provideHttpClient } from '@angular/common/http'; +import { NotificationsService } from './notifications.service'; +import { TableRowService } from './table-row.service'; describe('TableRowService', () => { - let service: TableRowService; - let httpMock: HttpTestingController; - - let fakeNotifications; - - const tableRowValues = { - "Id": 11, - "FirstName": "Yuriy" - } - - const tableRowNetwork = { - "row": tableRowValues, - "structure": [ - { - "column_name": "FirstName", - "column_default": null, - "data_type": "varchar", - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": false, - "character_maximum_length": 30 - }, - { - "column_name": "Id", - "column_default": null, - "data_type": "int", - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": false, - "character_maximum_length": 11 - } - ], - "foreignKeys": [], - "primaryColumns": [ - { - "data_type": "int", - "column_name": "Id" - } - ], - "readonly_fields": [], - "table_widgets": [] - } - - const fakeError = { - "message": "Table row error", - "statusCode": 400, - "type": "no_master_key", - "originalMessage": "Table row error details" - } - - beforeEach(() => { - fakeNotifications = { - showErrorSnackbar: vi.fn(), - showSuccessSnackbar: vi.fn(), - showAlert: vi.fn() - }; - - TestBed.configureTestingModule({ - imports: [MatSnackBarModule], - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - { - provide: NotificationsService, - useValue: fakeNotifications - }, - ] - }); - - service = TestBed.inject(TableRowService); - httpMock = TestBed.inject(HttpTestingController); - }); - - afterEach(() => { - httpMock.verify(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should call fetchTableRow', () => { - let isSubscribeCalled = false; - - service.fetchTableRow('12345678', 'users_table', {id: 1}).subscribe(res => { - expect(res).toEqual(tableRowNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/table/row/12345678?id=1&tableName=users_table`); - expect(req.request.method).toBe("GET"); - req.flush(tableRowNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should call addTableRow and show Success snackbar', () => { - let isSubscribeCalled = false; - - service.addTableRow('12345678', 'users_table', tableRowValues).subscribe(res => { - expect(res).toEqual(tableRowValues); - expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('The row has been added successfully to "users_table" table.'); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/table/row/12345678?tableName=users_table`); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual(tableRowValues); - req.flush(tableRowValues); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall addTableRow and show Error alert', async () => { - const addTableRow = service.addTableRow('12345678', 'users_table', tableRowValues).toPromise(); - - const req = httpMock.expectOne(`/table/row/12345678?tableName=users_table`); - expect(req.request.method).toBe("POST"); - req.flush(fakeError, {status: 400, statusText: ''}); - await addTableRow; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call updateTableRow and show Success snackbar', () => { - let isSubscribeCalled = false; - - service.updateTableRow('12345678', 'users_table', {id: 1}, tableRowValues).subscribe(_res => { - expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('The row has been updated successfully in "users_table" table.'); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/table/row/12345678?id=1&tableName=users_table`); - expect(req.request.method).toBe("PUT"); - expect(req.request.body).toEqual(tableRowValues); - req.flush(tableRowValues); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall updateTableRow and show Error alert', async () => { - const addTableRow = service.updateTableRow('12345678', 'users_table', {id: 1}, tableRowValues).toPromise(); - - const req = httpMock.expectOne(`/table/row/12345678?id=1&tableName=users_table`); - expect(req.request.method).toBe("PUT"); - req.flush(fakeError, {status: 400, statusText: ''}); - await addTableRow; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call deleteTableRow and show Success snackbar', () => { - let isSubscribeCalled = false; - - service.deleteTableRow('12345678', 'users_table', {id: 1}).subscribe(_res => { - expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Row has been deleted successfully from "users_table" table.'); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/table/row/12345678?id=1&tableName=users_table`); - expect(req.request.method).toBe("DELETE"); - req.flush({deleted: true}); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall deleteTableRow and show Error snackbar', async () => { - const deleteTableRow = service.deleteTableRow('12345678', 'users_table', {id: 1}).toPromise(); - - const req = httpMock.expectOne(`/table/row/12345678?id=1&tableName=users_table`); - expect(req.request.method).toBe("DELETE"); - req.flush(fakeError, {status: 400, statusText: ''}); - await deleteTableRow; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); + let service: TableRowService; + let httpMock: HttpTestingController; + + let fakeNotifications; + + const tableRowValues = { + Id: 11, + FirstName: 'Yuriy', + }; + + const tableRowNetwork = { + row: tableRowValues, + structure: [ + { + column_name: 'FirstName', + column_default: null, + data_type: 'varchar', + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: false, + character_maximum_length: 30, + }, + { + column_name: 'Id', + column_default: null, + data_type: 'int', + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: false, + character_maximum_length: 11, + }, + ], + foreignKeys: [], + primaryColumns: [ + { + data_type: 'int', + column_name: 'Id', + }, + ], + readonly_fields: [], + table_widgets: [], + }; + + const fakeError = { + message: 'Table row error', + statusCode: 400, + type: 'no_master_key', + originalMessage: 'Table row error details', + }; + + beforeEach(() => { + fakeNotifications = { + showErrorSnackbar: vi.fn(), + showSuccessSnackbar: vi.fn(), + showAlert: vi.fn(), + }; + + TestBed.configureTestingModule({ + imports: [MatSnackBarModule], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { + provide: NotificationsService, + useValue: fakeNotifications, + }, + ], + }); + + service = TestBed.inject(TableRowService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call fetchTableRow', () => { + let isSubscribeCalled = false; + + service.fetchTableRow('12345678', 'users_table', { id: 1 }).subscribe((res) => { + expect(res).toEqual(tableRowNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/table/row/12345678?id=1&tableName=users_table`); + expect(req.request.method).toBe('GET'); + req.flush(tableRowNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should call addTableRow and show Success snackbar', () => { + let isSubscribeCalled = false; + + service.addTableRow('12345678', 'users_table', tableRowValues).subscribe((res) => { + expect(res).toEqual(tableRowValues); + expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith( + 'The row has been added successfully to "users_table" table.', + ); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/table/row/12345678?tableName=users_table`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(tableRowValues); + req.flush(tableRowValues); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall addTableRow and show Error alert', async () => { + const addTableRow = service.addTableRow('12345678', 'users_table', tableRowValues).toPromise(); + + const req = httpMock.expectOne(`/table/row/12345678?tableName=users_table`); + expect(req.request.method).toBe('POST'); + req.flush(fakeError, { status: 400, statusText: '' }); + await addTableRow; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call updateTableRow and show Success snackbar', () => { + let isSubscribeCalled = false; + + service.updateTableRow('12345678', 'users_table', { id: 1 }, tableRowValues).subscribe((_res) => { + expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith( + 'The row has been updated successfully in "users_table" table.', + ); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/table/row/12345678?id=1&tableName=users_table`); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual(tableRowValues); + req.flush(tableRowValues); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall updateTableRow and show Error alert', async () => { + const addTableRow = service.updateTableRow('12345678', 'users_table', { id: 1 }, tableRowValues).toPromise(); + + const req = httpMock.expectOne(`/table/row/12345678?id=1&tableName=users_table`); + expect(req.request.method).toBe('PUT'); + req.flush(fakeError, { status: 400, statusText: '' }); + await addTableRow; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call deleteTableRow and show Success snackbar', () => { + let isSubscribeCalled = false; + + service.deleteTableRow('12345678', 'users_table', { id: 1 }).subscribe((_res) => { + expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith( + 'Row has been deleted successfully from "users_table" table.', + ); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/table/row/12345678?id=1&tableName=users_table`); + expect(req.request.method).toBe('DELETE'); + req.flush({ deleted: true }); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall deleteTableRow and show Error snackbar', async () => { + const deleteTableRow = service.deleteTableRow('12345678', 'users_table', { id: 1 }).toPromise(); + + const req = httpMock.expectOne(`/table/row/12345678?id=1&tableName=users_table`); + expect(req.request.method).toBe('DELETE'); + req.flush(fakeError, { status: 400, statusText: '' }); + await deleteTableRow; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); }); diff --git a/frontend/src/app/services/tables.service.spec.ts b/frontend/src/app/services/tables.service.spec.ts index 7ebb00ea4..e5dfcdf53 100644 --- a/frontend/src/app/services/tables.service.spec.ts +++ b/frontend/src/app/services/tables.service.spec.ts @@ -1,601 +1,630 @@ -import { AlertActionType, AlertType } from '../models/alert'; +import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; - -import { Angulartics2Module } from 'angulartics2'; +import { TestBed } from '@angular/core/testing'; import { MatSnackBarModule } from '@angular/material/snack-bar'; -import { NotificationsService } from './notifications.service'; +import { provideRouter } from '@angular/router'; +import { Angulartics2Module } from 'angulartics2'; +import { AlertActionType, AlertType } from '../models/alert'; import { TableOrdering } from '../models/table'; +import { NotificationsService } from './notifications.service'; import { TablesService } from './tables.service'; -import { TestBed } from '@angular/core/testing'; -import { provideHttpClient } from '@angular/common/http'; -import { provideRouter } from '@angular/router'; describe('TablesService', () => { - let service: TablesService; - let httpMock: HttpTestingController; - - let fakeNotifications; - - const structureNetwork = [ - { - "column_name": "id", - "column_default": "nextval('customers_id_seq'::regclass)", - "data_type": "integer", - "isExcluded": false, - "isSearched": false, - "auto_increment": true, - "allow_null": false, - "character_maximum_length": null - }, - { - "column_name": "firstname", - "column_default": null, - "data_type": "character varying", - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": true, - "character_maximum_length": 30 - }, - { - "column_name": "lastname", - "column_default": null, - "data_type": "character varying", - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": true, - "character_maximum_length": 30 - }, - { - "column_name": "email", - "column_default": null, - "data_type": "character varying", - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": false, - "character_maximum_length": 30 - }, - { - "column_name": "age", - "column_default": null, - "data_type": "integer", - "isExcluded": false, - "isSearched": false, - "auto_increment": false, - "allow_null": true, - "character_maximum_length": null - } - ] - - const usersTableNetwork = { - "rows": [ - { - "id": 33, - "firstname": "Alex", - "lastname": "Lichter", - "email": "new-user-5@email.com", - "age": 24 - }, - { - "id": 34, - "firstname": "Alex", - "lastname": "Lichter", - "email": "new-user-5@email.com", - "age": 24 - }, - { - "id": 35, - "firstname": "Alex", - "lastname": "Smith", - "email": "some-new@email.com", - "age": 24 - } - ], - "primaryColumns": [ - { - "column_name": "id", - "data_type": "integer" - } - ], - "pagination": { - "total": 30, - "lastPage": 1, - "perPage": 30, - "currentPage": 1 - }, - "sortable_by": [], - "ordering": "ASC", - "structure": structureNetwork, - "foreignKeys": [] - } - - const tableSettingsNetwork = { - "id": "dbf4d648-32f8-4202-9c18-300e1a4dc959", - "table_name": "contacts_with_uuid", - "display_name": "", - "search_fields": [], - "excluded_fields": [], - "list_fields": [], - "identification_fields": [], - "list_per_page": null, - "ordering": "ASC", - "ordering_field": "", - "identity_column": "", - "readonly_fields": [], - "sortable_by": [], - "autocomplete_columns": [ - "first_name", - "last_name", - "email" - ], - "columns_view": [ - "first_name", - "last_name", - "email" - ], - "connection_id": "e4b99271-badd-4112-9967-b99dd8024dda" - }; - - const tableSettingsApp = { - "autocomplete_columns": [ - "first_name", - "last_name", - "email" - ], - "columns_view": [ - "first_name", - "last_name", - "email" - ], - "connection_id": "e4b99271-badd-4112-9967-b99dd8024dda", - "display_name": "", - "icon": "", - "excluded_fields": [], - "id": "dbf4d648-32f8-4202-9c18-300e1a4dc959", - "identification_fields": [], - "identity_column": "", - "list_fields": [], - "list_per_page": null, - "ordering": TableOrdering.Descending, - "ordering_field": "", - "readonly_fields": [], - "search_fields": [], - "sortable_by": [], - "table_name": "contacts_with_uuid", - "sensitive_fields": [], - "allow_csv_export": true, - "allow_csv_import": true, - "can_delete": true, - }; - - const tableWidgetsNetwork = [ - { - "id": "a57e0c7f-a348-4aae-9ec4-fdbec0c0d0b6", - "field_name": "email", - "widget_type": "Textarea", - "widget_params": {}, - "name": "user email", - "description": "" - } - ] - - const tableWidgetsApp = [ - { - "description": "", - "field_name": "email", - "name": "user email", - "widget_params": "", - "widget_type": "Textarea" - } - ] - - const fakeError = { - "message": "Connection error", - "statusCode": 400, - "type": "no_master_key", - "originalMessage": "Connection error details", - } - - beforeEach(() => { - fakeNotifications = { - showErrorSnackbar: vi.fn(), - showSuccessSnackbar: vi.fn(), - showAlert: vi.fn() - }; - - TestBed.configureTestingModule({ - imports: [ - MatSnackBarModule, - Angulartics2Module.forRoot() - ], - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - provideRouter([]), - { - provide: NotificationsService, - useValue: fakeNotifications - }, - ] - }); - - service = TestBed.inject(TablesService); - httpMock = TestBed.inject(HttpTestingController); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should call setTableName', () => { - service.setTableName('users_table'); - expect(service.tableName).toEqual('users_table'); - }); - - it('should get currentTableName', () => { - service.tableName = 'users_table'; - expect(service.currentTableName).toEqual('users_table'); - }) - - it('should call fetchTables', () => { - let isSubscribeCalled = false; - const tablesNetwork = [ - { - "0": { - "table": "users", - "permissions": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - } - } - }, - { - "1": { - "table": "addresses", - "permissions": { - "visibility": true, - "readonly": false, - "add": true, - "delete": true, - "edit": true - } - } - } - ] - - service.fetchTables('12345678').subscribe(res => { - expect(res).toEqual(tablesNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/connection/tables/12345678`); - expect(req.request.method).toBe("GET"); - req.flush(tablesNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should call fetchTable with minimal params', () => { - let isSubscribeCalled = false; - - service.fetchTable({ - connectionID: '12345678', - tableName: 'users_table', - requstedPage: 1, - chunkSize: 30 - }).subscribe(res => { - expect(res).toEqual(usersTableNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/table/rows/find/12345678?tableName=users_table&perPage=30&page=1`); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual({ filters: undefined }); - req.flush(usersTableNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should call fetchTable for foreigh keys', () => { - let isSubscribeCalled = false; - - service.fetchTable({ - connectionID: '12345678', - tableName: 'users_table', - requstedPage: 1, - chunkSize: 30, - foreignKeyRowName: 'position_id', - foreignKeyRowValue: '9876', - referencedColumn: 'id' - }).subscribe(res => { - expect(res).toEqual(usersTableNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne( - `/table/rows/find/12345678?tableName=users_table&perPage=30&page=1&f_position_id__eq=9876&referencedColumn=id` - ); - expect(req.request.method).toBe("POST"); - req.flush(usersTableNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should call fetchTable with filters', () => { - let isSubscribeCalled = false; - - service.fetchTable({ - connectionID: '12345678', - tableName: 'users_table', - requstedPage: 1, - chunkSize: 30, - filters: { - city: { - eq: 'NewYork' - }, - age: { - eq: '42' - } - } - }).subscribe(res => { - expect(res).toEqual(usersTableNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne('/table/rows/find/12345678?tableName=users_table&perPage=30&page=1'); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual({ filters: { - city: { - eq: 'NewYork' - }, - age: { - eq: '42' - } - }}); - req.flush(usersTableNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should call fetchTable with sortOrder DESC and sort_by City column', () => { - let isSubscribeCalled = false; - - service.fetchTable({ - connectionID: '12345678', - tableName: 'users_table', - requstedPage: 1, - chunkSize: 30, - sortOrder: 'DESC', - sortColumn: 'city' - }).subscribe(res => { - expect(res).toEqual(usersTableNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne( - `/table/rows/find/12345678?tableName=users_table&perPage=30&page=1&sort_by=city&sort_order=DESC` - ); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual({ filters: undefined }); - req.flush(usersTableNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall fetchTable and show Error alert', async () => { - const fetchTableRow = service.fetchTable({ - connectionID: '12345678', - tableName: 'users_table', - requstedPage: 1, - chunkSize: 30 - }).toPromise(); - - const req = httpMock.expectOne(`/table/rows/find/12345678?tableName=users_table&perPage=30&page=1`); - expect(req.request.method).toBe("POST"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchTableRow; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - - it('should call fetchTableStructure', () => { - let isSubscribeCalled = false; - const tableStructureNetwork = { - "structure": structureNetwork, - "foreignKeys": [], - "readonly_fields": [], - "table_widgets": [] - }; - - service.fetchTableStructure('12345678', 'users_table').subscribe(res => { - expect(res).toEqual(tableStructureNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/table/structure/12345678?tableName=users_table`); - expect(req.request.method).toBe("GET"); - req.flush(tableStructureNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall fetchTableStructure and show Error snackbar', async () => { - const fetchTableStructure = service.fetchTableStructure('12345678', 'users_table').toPromise(); - - const req = httpMock.expectOne(`/table/structure/12345678?tableName=users_table`); - expect(req.request.method).toBe("GET"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchTableStructure; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - - it('should call fetchTableSettings', () => { - let isSubscribeCalled = false; - - service.fetchTableSettings('12345678', 'users_table').subscribe(res => { - expect(res).toEqual(tableSettingsNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); - expect(req.request.method).toBe("GET"); - req.flush(tableSettingsNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall fetchTableSettings and show Error snackbar', async () => { - const fetchTableSettings = service.fetchTableSettings('12345678', 'users_table').toPromise(); - - const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); - expect(req.request.method).toBe("GET"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchTableSettings; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call updateTableSettings for existing settings', () => { - let isSubscribeCalled = false; - - service.updateTableSettings(true, '12345678', 'users_table', tableSettingsApp).subscribe(_res => { - // expect(res).toEqual(tableSettingsNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); - expect(req.request.method).toBe("PUT"); - expect(req.request.body).toEqual(tableSettingsApp); - req.flush(tableSettingsNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should call updateTableSettings and create settings', () => { - let isSubscribeCalled = false; - - service.updateTableSettings(false, '12345678', 'users_table', tableSettingsApp).subscribe(_res => { - // expect(res).toEqual(tableSettingsNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual(tableSettingsApp); - req.flush(tableSettingsNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall updateTableSettings and show Error alert', async () => { - const fetchTableSettings = service.updateTableSettings(true, '12345678', 'users_table', tableSettingsApp).toPromise(); - - const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); - expect(req.request.method).toBe("PUT"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchTableSettings; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call deleteTableSettings', () => { - let isSubscribeCalled = false; - - service.deleteTableSettings('12345678', 'users_table').subscribe(_res => { - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); - expect(req.request.method).toBe("DELETE"); - req.flush(tableSettingsNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall deleteTableSettings and show Error snackbar', async () => { - const fetchTableSettings = service.deleteTableSettings('12345678', 'users_table').toPromise(); - - const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); - expect(req.request.method).toBe("DELETE"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchTableSettings; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call fetchTableWidgets', () => { - let isSubscribeCalled = false; - - service.fetchTableWidgets('12345678', 'users_table').subscribe(_res => { - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/widgets/12345678?tableName=users_table`); - expect(req.request.method).toBe("GET"); - req.flush(tableSettingsNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall fetchTableWidgets and show Error alert', async () => { - const fetchTableSettings = service.fetchTableWidgets('12345678', 'users_table').toPromise(); - - const req = httpMock.expectOne(`/widgets/12345678?tableName=users_table`); - expect(req.request.method).toBe("GET"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchTableSettings; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); - - it('should call updateTableWidgets', () => { - let isSubscribeCalled = false; - - service.updateTableWidgets('12345678', 'users_table', tableWidgetsApp).subscribe(res => { - expect(res).toEqual(tableWidgetsNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne('/widget/12345678?tableName=users_table'); - expect(req.request.method).toBe("POST"); - req.flush(tableWidgetsNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall updateTableWidgets and show Error bannner', async () => { - const fetchTableSettings = service.updateTableWidgets('12345678', 'users_table', tableWidgetsApp).toPromise(); - - const req = httpMock.expectOne('/widget/12345678?tableName=users_table'); - expect(req.request.method).toBe("POST"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchTableSettings; - - expect(fakeNotifications.showAlert).toHaveBeenCalledWith(AlertType.Error, { abstract: fakeError.message, details: fakeError.originalMessage }, [expect.objectContaining({ - type: AlertActionType.Button, - caption: 'Dismiss', - })]); - }); + let service: TablesService; + let httpMock: HttpTestingController; + + let fakeNotifications; + + const structureNetwork = [ + { + column_name: 'id', + column_default: "nextval('customers_id_seq'::regclass)", + data_type: 'integer', + isExcluded: false, + isSearched: false, + auto_increment: true, + allow_null: false, + character_maximum_length: null, + }, + { + column_name: 'firstname', + column_default: null, + data_type: 'character varying', + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: true, + character_maximum_length: 30, + }, + { + column_name: 'lastname', + column_default: null, + data_type: 'character varying', + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: true, + character_maximum_length: 30, + }, + { + column_name: 'email', + column_default: null, + data_type: 'character varying', + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: false, + character_maximum_length: 30, + }, + { + column_name: 'age', + column_default: null, + data_type: 'integer', + isExcluded: false, + isSearched: false, + auto_increment: false, + allow_null: true, + character_maximum_length: null, + }, + ]; + + const usersTableNetwork = { + rows: [ + { + id: 33, + firstname: 'Alex', + lastname: 'Lichter', + email: 'new-user-5@email.com', + age: 24, + }, + { + id: 34, + firstname: 'Alex', + lastname: 'Lichter', + email: 'new-user-5@email.com', + age: 24, + }, + { + id: 35, + firstname: 'Alex', + lastname: 'Smith', + email: 'some-new@email.com', + age: 24, + }, + ], + primaryColumns: [ + { + column_name: 'id', + data_type: 'integer', + }, + ], + pagination: { + total: 30, + lastPage: 1, + perPage: 30, + currentPage: 1, + }, + sortable_by: [], + ordering: 'ASC', + structure: structureNetwork, + foreignKeys: [], + }; + + const tableSettingsNetwork = { + id: 'dbf4d648-32f8-4202-9c18-300e1a4dc959', + table_name: 'contacts_with_uuid', + display_name: '', + search_fields: [], + excluded_fields: [], + list_fields: [], + identification_fields: [], + list_per_page: null, + ordering: 'ASC', + ordering_field: '', + identity_column: '', + readonly_fields: [], + sortable_by: [], + autocomplete_columns: ['first_name', 'last_name', 'email'], + columns_view: ['first_name', 'last_name', 'email'], + connection_id: 'e4b99271-badd-4112-9967-b99dd8024dda', + }; + + const tableSettingsApp = { + autocomplete_columns: ['first_name', 'last_name', 'email'], + columns_view: ['first_name', 'last_name', 'email'], + connection_id: 'e4b99271-badd-4112-9967-b99dd8024dda', + display_name: '', + icon: '', + excluded_fields: [], + id: 'dbf4d648-32f8-4202-9c18-300e1a4dc959', + identification_fields: [], + identity_column: '', + list_fields: [], + list_per_page: null, + ordering: TableOrdering.Descending, + ordering_field: '', + readonly_fields: [], + search_fields: [], + sortable_by: [], + table_name: 'contacts_with_uuid', + sensitive_fields: [], + allow_csv_export: true, + allow_csv_import: true, + can_delete: true, + }; + + const tableWidgetsNetwork = [ + { + id: 'a57e0c7f-a348-4aae-9ec4-fdbec0c0d0b6', + field_name: 'email', + widget_type: 'Textarea', + widget_params: {}, + name: 'user email', + description: '', + }, + ]; + + const tableWidgetsApp = [ + { + description: '', + field_name: 'email', + name: 'user email', + widget_params: '', + widget_type: 'Textarea', + }, + ]; + + const fakeError = { + message: 'Connection error', + statusCode: 400, + type: 'no_master_key', + originalMessage: 'Connection error details', + }; + + beforeEach(() => { + fakeNotifications = { + showErrorSnackbar: vi.fn(), + showSuccessSnackbar: vi.fn(), + showAlert: vi.fn(), + }; + + TestBed.configureTestingModule({ + imports: [MatSnackBarModule, Angulartics2Module.forRoot()], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + provideRouter([]), + { + provide: NotificationsService, + useValue: fakeNotifications, + }, + ], + }); + + service = TestBed.inject(TablesService); + httpMock = TestBed.inject(HttpTestingController); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call setTableName', () => { + service.setTableName('users_table'); + expect(service.tableName).toEqual('users_table'); + }); + + it('should get currentTableName', () => { + service.tableName = 'users_table'; + expect(service.currentTableName).toEqual('users_table'); + }); + + it('should call fetchTables', () => { + let isSubscribeCalled = false; + const tablesNetwork = [ + { + '0': { + table: 'users', + permissions: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + }, + }, + { + '1': { + table: 'addresses', + permissions: { + visibility: true, + readonly: false, + add: true, + delete: true, + edit: true, + }, + }, + }, + ]; + + service.fetchTables('12345678').subscribe((res) => { + expect(res).toEqual(tablesNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/connection/tables/12345678`); + expect(req.request.method).toBe('GET'); + req.flush(tablesNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should call fetchTable with minimal params', () => { + let isSubscribeCalled = false; + + service + .fetchTable({ + connectionID: '12345678', + tableName: 'users_table', + requstedPage: 1, + chunkSize: 30, + }) + .subscribe((res) => { + expect(res).toEqual(usersTableNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/table/rows/find/12345678?tableName=users_table&perPage=30&page=1`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ filters: undefined }); + req.flush(usersTableNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should call fetchTable for foreigh keys', () => { + let isSubscribeCalled = false; + + service + .fetchTable({ + connectionID: '12345678', + tableName: 'users_table', + requstedPage: 1, + chunkSize: 30, + foreignKeyRowName: 'position_id', + foreignKeyRowValue: '9876', + referencedColumn: 'id', + }) + .subscribe((res) => { + expect(res).toEqual(usersTableNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne( + `/table/rows/find/12345678?tableName=users_table&perPage=30&page=1&f_position_id__eq=9876&referencedColumn=id`, + ); + expect(req.request.method).toBe('POST'); + req.flush(usersTableNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should call fetchTable with filters', () => { + let isSubscribeCalled = false; + + service + .fetchTable({ + connectionID: '12345678', + tableName: 'users_table', + requstedPage: 1, + chunkSize: 30, + filters: { + city: { + eq: 'NewYork', + }, + age: { + eq: '42', + }, + }, + }) + .subscribe((res) => { + expect(res).toEqual(usersTableNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne('/table/rows/find/12345678?tableName=users_table&perPage=30&page=1'); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + filters: { + city: { + eq: 'NewYork', + }, + age: { + eq: '42', + }, + }, + }); + req.flush(usersTableNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should call fetchTable with sortOrder DESC and sort_by City column', () => { + let isSubscribeCalled = false; + + service + .fetchTable({ + connectionID: '12345678', + tableName: 'users_table', + requstedPage: 1, + chunkSize: 30, + sortOrder: 'DESC', + sortColumn: 'city', + }) + .subscribe((res) => { + expect(res).toEqual(usersTableNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne( + `/table/rows/find/12345678?tableName=users_table&perPage=30&page=1&sort_by=city&sort_order=DESC`, + ); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ filters: undefined }); + req.flush(usersTableNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall fetchTable and show Error alert', async () => { + const fetchTableRow = service + .fetchTable({ + connectionID: '12345678', + tableName: 'users_table', + requstedPage: 1, + chunkSize: 30, + }) + .toPromise(); + + const req = httpMock.expectOne(`/table/rows/find/12345678?tableName=users_table&perPage=30&page=1`); + expect(req.request.method).toBe('POST'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchTableRow; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call fetchTableStructure', () => { + let isSubscribeCalled = false; + const tableStructureNetwork = { + structure: structureNetwork, + foreignKeys: [], + readonly_fields: [], + table_widgets: [], + }; + + service.fetchTableStructure('12345678', 'users_table').subscribe((res) => { + expect(res).toEqual(tableStructureNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/table/structure/12345678?tableName=users_table`); + expect(req.request.method).toBe('GET'); + req.flush(tableStructureNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall fetchTableStructure and show Error snackbar', async () => { + const fetchTableStructure = service.fetchTableStructure('12345678', 'users_table').toPromise(); + + const req = httpMock.expectOne(`/table/structure/12345678?tableName=users_table`); + expect(req.request.method).toBe('GET'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchTableStructure; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + + it('should call fetchTableSettings', () => { + let isSubscribeCalled = false; + + service.fetchTableSettings('12345678', 'users_table').subscribe((res) => { + expect(res).toEqual(tableSettingsNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); + expect(req.request.method).toBe('GET'); + req.flush(tableSettingsNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall fetchTableSettings and show Error snackbar', async () => { + const fetchTableSettings = service.fetchTableSettings('12345678', 'users_table').toPromise(); + + const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); + expect(req.request.method).toBe('GET'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchTableSettings; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call updateTableSettings for existing settings', () => { + let isSubscribeCalled = false; + + service.updateTableSettings(true, '12345678', 'users_table', tableSettingsApp).subscribe((_res) => { + // expect(res).toEqual(tableSettingsNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual(tableSettingsApp); + req.flush(tableSettingsNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should call updateTableSettings and create settings', () => { + let isSubscribeCalled = false; + + service.updateTableSettings(false, '12345678', 'users_table', tableSettingsApp).subscribe((_res) => { + // expect(res).toEqual(tableSettingsNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(tableSettingsApp); + req.flush(tableSettingsNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall updateTableSettings and show Error alert', async () => { + const fetchTableSettings = service + .updateTableSettings(true, '12345678', 'users_table', tableSettingsApp) + .toPromise(); + + const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); + expect(req.request.method).toBe('PUT'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchTableSettings; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call deleteTableSettings', () => { + let isSubscribeCalled = false; + + service.deleteTableSettings('12345678', 'users_table').subscribe((_res) => { + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); + expect(req.request.method).toBe('DELETE'); + req.flush(tableSettingsNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall deleteTableSettings and show Error snackbar', async () => { + const fetchTableSettings = service.deleteTableSettings('12345678', 'users_table').toPromise(); + + const req = httpMock.expectOne(`/settings?connectionId=12345678&tableName=users_table`); + expect(req.request.method).toBe('DELETE'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchTableSettings; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call fetchTableWidgets', () => { + let isSubscribeCalled = false; + + service.fetchTableWidgets('12345678', 'users_table').subscribe((_res) => { + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/widgets/12345678?tableName=users_table`); + expect(req.request.method).toBe('GET'); + req.flush(tableSettingsNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall fetchTableWidgets and show Error alert', async () => { + const fetchTableSettings = service.fetchTableWidgets('12345678', 'users_table').toPromise(); + + const req = httpMock.expectOne(`/widgets/12345678?tableName=users_table`); + expect(req.request.method).toBe('GET'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchTableSettings; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); + + it('should call updateTableWidgets', () => { + let isSubscribeCalled = false; + + service.updateTableWidgets('12345678', 'users_table', tableWidgetsApp).subscribe((res) => { + expect(res).toEqual(tableWidgetsNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne('/widget/12345678?tableName=users_table'); + expect(req.request.method).toBe('POST'); + req.flush(tableWidgetsNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall updateTableWidgets and show Error bannner', async () => { + const fetchTableSettings = service.updateTableWidgets('12345678', 'users_table', tableWidgetsApp).toPromise(); + + const req = httpMock.expectOne('/widget/12345678?tableName=users_table'); + expect(req.request.method).toBe('POST'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchTableSettings; + + expect(fakeNotifications.showAlert).toHaveBeenCalledWith( + AlertType.Error, + { abstract: fakeError.message, details: fakeError.originalMessage }, + [ + expect.objectContaining({ + type: AlertActionType.Button, + caption: 'Dismiss', + }), + ], + ); + }); }); diff --git a/frontend/src/app/services/users.service.spec.ts b/frontend/src/app/services/users.service.spec.ts index 91ac87ddc..b865070b5 100644 --- a/frontend/src/app/services/users.service.spec.ts +++ b/frontend/src/app/services/users.service.spec.ts @@ -1,411 +1,419 @@ +import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; import { TestBed } from '@angular/core/testing'; -import { UsersService } from './users.service'; -import { NotificationsService } from './notifications.service'; -import { AccessLevel } from '../models/user'; -import { provideHttpClient } from '@angular/common/http'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; import { provideRouter } from '@angular/router'; +import { AccessLevel } from '../models/user'; +import { NotificationsService } from './notifications.service'; +import { UsersService } from './users.service'; describe('UsersService', () => { - let service: UsersService; - let httpMock: HttpTestingController; - - let fakeNotifications; - - const groupNetwork = { - "title": "Managers", - "users": [ - { - "id": "83f35e11-6499-470e-9ccb-08b6d9393943", - "createdAt": "2021-07-21T14:35:17.270Z", - "gclid": null, - "isActive": true - } - ], - "id": "1c042912-326d-4fc5-bb0c-10da88dd37c4", - "isMain": false - } - - const permissionsNetwork = { - "connection": { - "connectionId": "75b0574a-9fc5-4472-90e1-5c030b0b28b5", - "accessLevel": "readonly" - }, - "group": { - "groupId": "1c042912-326d-4fc5-bb0c-10da88dd37c4", - "accessLevel": "edit" - }, - "tables": [ - { - "tableName": "TOYS_TEST", - "accessLevel": { - "visibility": true, - "readonly": true, - "add": false, - "delete": false, - "edit": false - } - }, - { - "tableName": "PRODUCTS_TEST", - "accessLevel": { - "visibility": true, - "readonly": false, - "add": true, - "delete": false, - "edit": true - } - } - ] - } - - const permissionsApp = { - "connection": { - "accessLevel": AccessLevel.Readonly, - "connectionId": "75b0574a-9fc5-4472-90e1-5c030b0b28b5" - }, - "group": { - "accessLevel": AccessLevel.Edit, - "groupId": "1c042912-326d-4fc5-bb0c-10da88dd37c4" - }, - "tables": [ - { - "accessLevel": { - "add": false, - "delete": false, - "edit": false, - "readonly": true, - "visibility": true - }, - "tableName": "TOYS_TEST", - display_name: "Toys tests" - }, - { - "accessLevel": { - "add": true, - "delete": false, - "edit": true, - "readonly": false, - "visibility": true - }, - "tableName": "PRODUCTS_TEST", - display_name: "Product tests" - } - ] - } - - const fakeError = { - "message": "Connection error", - "statusCode": 400, - "type": "no_master_key" - } - - beforeEach(() => { - fakeNotifications = { - showErrorSnackbar: vi.fn(), - showSuccessSnackbar: vi.fn(), - }; - - TestBed.configureTestingModule({ - imports: [MatSnackBarModule], - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - provideRouter([]), - { - provide: NotificationsService, - useValue: fakeNotifications - }, - ] - }); - - service = TestBed.inject(UsersService); - httpMock = TestBed.inject(HttpTestingController); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should call fetchConnectionUsers', () => { - let isSubscribeCalled = false; - const usersNetwork = [ - { - "id": "83f35e11-6499-470e-9ccb-08b6d9393943", - "isActive": true, - "email": "lyubov+fghj@voloshko.com", - "createdAt": "2021-07-21T14:35:17.270Z" - } - ]; - - service.fetchConnectionUsers('12345678').subscribe(res => { - expect(res).toEqual(usersNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/connection/users/12345678`); - expect(req.request.method).toBe("GET"); - req.flush(usersNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall fetchConnectionUsers and show Error snackbar', async () => { - const fetchConnectionUsers = service.fetchConnectionUsers('12345678').toPromise(); - - const req = httpMock.expectOne(`/connection/users/12345678`); - expect(req.request.method).toBe("GET"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchConnectionUsers; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - - it('should call fetchConnectionGroups', () => { - let isSubscribeCalled = false; - const groupsNetwork = [ - { - "group": { - "id": "014fa4ae-f56f-4084-ac24-58296641678b", - "title": "Admin", - "isMain": true - }, - "accessLevel": "edit" - } - ]; - - service.fetchConnectionGroups('12345678').subscribe(res => { - expect(res).toEqual(groupsNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/connection/groups/12345678`); - expect(req.request.method).toBe("GET"); - req.flush(groupsNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall fetchConnectionGroups and show Error snackbar', async () => { // Updated test case - const fetchConnectionGroups = service.fetchConnectionGroups('12345678').toPromise(); - - const req = httpMock.expectOne(`/connection/groups/12345678`); - expect(req.request.method).toBe("GET"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchConnectionGroups; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - - it('should call fetcGroupUsers', () => { - let isSubscribeCalled = false; - const groupUsersNetwork = [ - { - "id": "83f35e11-6499-470e-9ccb-08b6d9393943", - "createdAt": "2021-07-21T14:35:17.270Z", - "gclid": null, - "isActive": true, - "email": "lyubov+fghj@voloshko.com" - } - ]; - - service.fetcGroupUsers('12345678').subscribe(res => { - expect(res).toEqual(groupUsersNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/group/users/12345678`); - expect(req.request.method).toBe("GET"); - req.flush(groupUsersNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall fetchConnectionGroups and show Error snackbar', async () => { // Updated test case - const fetchConnectionGroups = service.fetcGroupUsers('12345678').toPromise(); - - const req = httpMock.expectOne(`/group/users/12345678`); - expect(req.request.method).toBe("GET"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchConnectionGroups; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - - it('should call createUsersGroup', () => { - let isSubscribeCalled = false; - - service.createUsersGroup('12345678', 'Managers').subscribe(_res => { - expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Group of users has been created.'); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/connection/group/12345678`); - expect(req.request.method).toBe("POST"); - expect(req.request.body).toEqual({title: 'Managers'}); - req.flush(groupNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall createUsersGroup and show Error snackbar', async () => { // Updated test case - const createUsersGroup = service.createUsersGroup('12345678', 'Managers').toPromise(); - - const req = httpMock.expectOne(`/connection/group/12345678`); - expect(req.request.method).toBe("POST"); - req.flush(fakeError, {status: 400, statusText: ''}); - await createUsersGroup; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - - it('should call fetchPermission', () => { - let isSubscribeCalled = false; - - service.fetchPermission('12345678', 'group12345678').subscribe(res => { - expect(res).toEqual(permissionsNetwork); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/connection/permissions?connectionId=12345678&groupId=group12345678`); - expect(req.request.method).toBe("GET"); - req.flush(permissionsNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall fetchPermission and show Error snackbar', async () => { // Updated test case - const fetchPermission = service.fetchPermission('12345678', 'group12345678').toPromise(); - - const req = httpMock.expectOne(`/connection/permissions?connectionId=12345678&groupId=group12345678`); - expect(req.request.method).toBe("GET"); - req.flush(fakeError, {status: 400, statusText: ''}); - await fetchPermission; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - - it('should call updatePermission and show Success snackbar', () => { - let isSubscribeCalled = false; - - service.updatePermission('12345678', permissionsApp).subscribe(_res => { - expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Permissions have been updated successfully.'); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/permissions/1c042912-326d-4fc5-bb0c-10da88dd37c4?connectionId=12345678`); - expect(req.request.method).toBe("PUT"); - expect(req.request.body).toEqual({permissions: permissionsApp}); - req.flush(permissionsNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall updatePermission and show Error snackbar', async () => { // Updated test case - const updatePermission = service.updatePermission('12345678', permissionsApp).toPromise(); - - const req = httpMock.expectOne(`/permissions/1c042912-326d-4fc5-bb0c-10da88dd37c4?connectionId=12345678`); - expect(req.request.method).toBe("PUT"); - req.flush(fakeError, {status: 400, statusText: ''}); - await updatePermission; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - - it('should call addGroupUser and show Success snackbar', () => { - let isSubscribeCalled = false; - - service.addGroupUser('group12345678', 'eric.cartman@south.park').subscribe(_res => { - // expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('User has been added to group.'); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/group/user`); - expect(req.request.method).toBe("PUT"); - expect(req.request.body).toEqual({ - email: 'eric.cartman@south.park', - groupId: 'group12345678' - }); - req.flush(groupNetwork); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall addGroupUser and show Error snackbar', async () => { // Updated test case - const addGroupUser = service.addGroupUser('group12345678', 'eric.cartman@south.park').toPromise(); - - const req = httpMock.expectOne(`/group/user`); - expect(req.request.method).toBe("PUT"); - req.flush(fakeError, {status: 400, statusText: ''}); - await addGroupUser; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - - it('should call deleteUsersGroup and show Success snackbar', () => { - let isSubscribeCalled = false; - - const deleteGroup = { - "raw": [], - "affected": 1 - } - - service.deleteUsersGroup('group12345678').subscribe(_res => { - expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Group has been removed.'); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/group/group12345678`); - expect(req.request.method).toBe("DELETE"); - req.flush(deleteGroup); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall deleteUsersGroup and show Error snackbar', async () => { // Updated test case - const deleteUsersGroup = service.deleteUsersGroup('group12345678').toPromise(); - - const req = httpMock.expectOne(`/group/group12345678`); - expect(req.request.method).toBe("DELETE"); - req.flush(fakeError, {status: 400, statusText: ''}); - await deleteUsersGroup; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); - - it('should call deleteGroupUser and show Success snackbar', () => { - let isSubscribeCalled = false; - - const deleteGroup = { - "raw": [], - "affected": 1 - } - - service.deleteGroupUser('eric.cartman@south.park', 'group12345678').subscribe(_res => { - expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('User has been removed from group.'); - isSubscribeCalled = true; - }); - - const req = httpMock.expectOne(`/group/user/delete`); - expect(req.request.method).toBe("PUT"); - expect(req.request.body).toEqual({ - email: 'eric.cartman@south.park', - groupId: 'group12345678' - }); - req.flush(deleteGroup); - - expect(isSubscribeCalled).toBe(true); - }); - - it('should fall deleteGroupUser and show Error snackbar', async () => { // Updated test case - const deleteGroupUser = service.deleteGroupUser('eric.cartman@south.park', 'group12345678').toPromise(); - - const req = httpMock.expectOne(`/group/user/delete`); - expect(req.request.method).toBe("PUT"); - req.flush(fakeError, {status: 400, statusText: ''}); - await deleteGroupUser; - - expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); - }); + let service: UsersService; + let httpMock: HttpTestingController; + + let fakeNotifications; + + const groupNetwork = { + title: 'Managers', + users: [ + { + id: '83f35e11-6499-470e-9ccb-08b6d9393943', + createdAt: '2021-07-21T14:35:17.270Z', + gclid: null, + isActive: true, + }, + ], + id: '1c042912-326d-4fc5-bb0c-10da88dd37c4', + isMain: false, + }; + + const permissionsNetwork = { + connection: { + connectionId: '75b0574a-9fc5-4472-90e1-5c030b0b28b5', + accessLevel: 'readonly', + }, + group: { + groupId: '1c042912-326d-4fc5-bb0c-10da88dd37c4', + accessLevel: 'edit', + }, + tables: [ + { + tableName: 'TOYS_TEST', + accessLevel: { + visibility: true, + readonly: true, + add: false, + delete: false, + edit: false, + }, + }, + { + tableName: 'PRODUCTS_TEST', + accessLevel: { + visibility: true, + readonly: false, + add: true, + delete: false, + edit: true, + }, + }, + ], + }; + + const permissionsApp = { + connection: { + accessLevel: AccessLevel.Readonly, + connectionId: '75b0574a-9fc5-4472-90e1-5c030b0b28b5', + }, + group: { + accessLevel: AccessLevel.Edit, + groupId: '1c042912-326d-4fc5-bb0c-10da88dd37c4', + }, + tables: [ + { + accessLevel: { + add: false, + delete: false, + edit: false, + readonly: true, + visibility: true, + }, + tableName: 'TOYS_TEST', + display_name: 'Toys tests', + }, + { + accessLevel: { + add: true, + delete: false, + edit: true, + readonly: false, + visibility: true, + }, + tableName: 'PRODUCTS_TEST', + display_name: 'Product tests', + }, + ], + }; + + const fakeError = { + message: 'Connection error', + statusCode: 400, + type: 'no_master_key', + }; + + beforeEach(() => { + fakeNotifications = { + showErrorSnackbar: vi.fn(), + showSuccessSnackbar: vi.fn(), + }; + + TestBed.configureTestingModule({ + imports: [MatSnackBarModule], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + provideRouter([]), + { + provide: NotificationsService, + useValue: fakeNotifications, + }, + ], + }); + + service = TestBed.inject(UsersService); + httpMock = TestBed.inject(HttpTestingController); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call fetchConnectionUsers', () => { + let isSubscribeCalled = false; + const usersNetwork = [ + { + id: '83f35e11-6499-470e-9ccb-08b6d9393943', + isActive: true, + email: 'lyubov+fghj@voloshko.com', + createdAt: '2021-07-21T14:35:17.270Z', + }, + ]; + + service.fetchConnectionUsers('12345678').subscribe((res) => { + expect(res).toEqual(usersNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/connection/users/12345678`); + expect(req.request.method).toBe('GET'); + req.flush(usersNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall fetchConnectionUsers and show Error snackbar', async () => { + const fetchConnectionUsers = service.fetchConnectionUsers('12345678').toPromise(); + + const req = httpMock.expectOne(`/connection/users/12345678`); + expect(req.request.method).toBe('GET'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchConnectionUsers; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + + it('should call fetchConnectionGroups', () => { + let isSubscribeCalled = false; + const groupsNetwork = [ + { + group: { + id: '014fa4ae-f56f-4084-ac24-58296641678b', + title: 'Admin', + isMain: true, + }, + accessLevel: 'edit', + }, + ]; + + service.fetchConnectionGroups('12345678').subscribe((res) => { + expect(res).toEqual(groupsNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/connection/groups/12345678`); + expect(req.request.method).toBe('GET'); + req.flush(groupsNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall fetchConnectionGroups and show Error snackbar', async () => { + // Updated test case + const fetchConnectionGroups = service.fetchConnectionGroups('12345678').toPromise(); + + const req = httpMock.expectOne(`/connection/groups/12345678`); + expect(req.request.method).toBe('GET'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchConnectionGroups; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + + it('should call fetcGroupUsers', () => { + let isSubscribeCalled = false; + const groupUsersNetwork = [ + { + id: '83f35e11-6499-470e-9ccb-08b6d9393943', + createdAt: '2021-07-21T14:35:17.270Z', + gclid: null, + isActive: true, + email: 'lyubov+fghj@voloshko.com', + }, + ]; + + service.fetcGroupUsers('12345678').subscribe((res) => { + expect(res).toEqual(groupUsersNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/group/users/12345678`); + expect(req.request.method).toBe('GET'); + req.flush(groupUsersNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall fetchConnectionGroups and show Error snackbar', async () => { + // Updated test case + const fetchConnectionGroups = service.fetcGroupUsers('12345678').toPromise(); + + const req = httpMock.expectOne(`/group/users/12345678`); + expect(req.request.method).toBe('GET'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchConnectionGroups; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + + it('should call createUsersGroup', () => { + let isSubscribeCalled = false; + + service.createUsersGroup('12345678', 'Managers').subscribe((_res) => { + expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Group of users has been created.'); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/connection/group/12345678`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ title: 'Managers' }); + req.flush(groupNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall createUsersGroup and show Error snackbar', async () => { + // Updated test case + const createUsersGroup = service.createUsersGroup('12345678', 'Managers').toPromise(); + + const req = httpMock.expectOne(`/connection/group/12345678`); + expect(req.request.method).toBe('POST'); + req.flush(fakeError, { status: 400, statusText: '' }); + await createUsersGroup; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + + it('should call fetchPermission', () => { + let isSubscribeCalled = false; + + service.fetchPermission('12345678', 'group12345678').subscribe((res) => { + expect(res).toEqual(permissionsNetwork); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/connection/permissions?connectionId=12345678&groupId=group12345678`); + expect(req.request.method).toBe('GET'); + req.flush(permissionsNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall fetchPermission and show Error snackbar', async () => { + // Updated test case + const fetchPermission = service.fetchPermission('12345678', 'group12345678').toPromise(); + + const req = httpMock.expectOne(`/connection/permissions?connectionId=12345678&groupId=group12345678`); + expect(req.request.method).toBe('GET'); + req.flush(fakeError, { status: 400, statusText: '' }); + await fetchPermission; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + + it('should call updatePermission and show Success snackbar', () => { + let isSubscribeCalled = false; + + service.updatePermission('12345678', permissionsApp).subscribe((_res) => { + expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Permissions have been updated successfully.'); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/permissions/1c042912-326d-4fc5-bb0c-10da88dd37c4?connectionId=12345678`); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual({ permissions: permissionsApp }); + req.flush(permissionsNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall updatePermission and show Error snackbar', async () => { + // Updated test case + const updatePermission = service.updatePermission('12345678', permissionsApp).toPromise(); + + const req = httpMock.expectOne(`/permissions/1c042912-326d-4fc5-bb0c-10da88dd37c4?connectionId=12345678`); + expect(req.request.method).toBe('PUT'); + req.flush(fakeError, { status: 400, statusText: '' }); + await updatePermission; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + + it('should call addGroupUser and show Success snackbar', () => { + let isSubscribeCalled = false; + + service.addGroupUser('group12345678', 'eric.cartman@south.park').subscribe((_res) => { + // expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('User has been added to group.'); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/group/user`); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual({ + email: 'eric.cartman@south.park', + groupId: 'group12345678', + }); + req.flush(groupNetwork); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall addGroupUser and show Error snackbar', async () => { + // Updated test case + const addGroupUser = service.addGroupUser('group12345678', 'eric.cartman@south.park').toPromise(); + + const req = httpMock.expectOne(`/group/user`); + expect(req.request.method).toBe('PUT'); + req.flush(fakeError, { status: 400, statusText: '' }); + await addGroupUser; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + + it('should call deleteUsersGroup and show Success snackbar', () => { + let isSubscribeCalled = false; + + const deleteGroup = { + raw: [], + affected: 1, + }; + + service.deleteUsersGroup('group12345678').subscribe((_res) => { + expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('Group has been removed.'); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/group/group12345678`); + expect(req.request.method).toBe('DELETE'); + req.flush(deleteGroup); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall deleteUsersGroup and show Error snackbar', async () => { + // Updated test case + const deleteUsersGroup = service.deleteUsersGroup('group12345678').toPromise(); + + const req = httpMock.expectOne(`/group/group12345678`); + expect(req.request.method).toBe('DELETE'); + req.flush(fakeError, { status: 400, statusText: '' }); + await deleteUsersGroup; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); + + it('should call deleteGroupUser and show Success snackbar', () => { + let isSubscribeCalled = false; + + const deleteGroup = { + raw: [], + affected: 1, + }; + + service.deleteGroupUser('eric.cartman@south.park', 'group12345678').subscribe((_res) => { + expect(fakeNotifications.showSuccessSnackbar).toHaveBeenCalledWith('User has been removed from group.'); + isSubscribeCalled = true; + }); + + const req = httpMock.expectOne(`/group/user/delete`); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual({ + email: 'eric.cartman@south.park', + groupId: 'group12345678', + }); + req.flush(deleteGroup); + + expect(isSubscribeCalled).toBe(true); + }); + + it('should fall deleteGroupUser and show Error snackbar', async () => { + // Updated test case + const deleteGroupUser = service.deleteGroupUser('eric.cartman@south.park', 'group12345678').toPromise(); + + const req = httpMock.expectOne(`/group/user/delete`); + expect(req.request.method).toBe('PUT'); + req.flush(fakeError, { status: 400, statusText: '' }); + await deleteGroupUser; + + expect(fakeNotifications.showErrorSnackbar).toHaveBeenCalledWith(fakeError.message); + }); }); diff --git a/frontend/src/app/types/turnstile.d.ts b/frontend/src/app/types/turnstile.d.ts new file mode 100644 index 000000000..15e4a2a50 --- /dev/null +++ b/frontend/src/app/types/turnstile.d.ts @@ -0,0 +1,22 @@ +export interface TurnstileOptions { + sitekey: string; + callback?: (token: string) => void; + 'error-callback'?: () => void; + 'expired-callback'?: () => void; + theme?: 'light' | 'dark' | 'auto'; + appearance?: 'always' | 'execute' | 'interaction-only'; + size?: 'normal' | 'compact'; +} + +export interface TurnstileInstance { + render: (container: string | HTMLElement, options: TurnstileOptions) => string; + reset: (widgetId?: string) => void; + getResponse: (widgetId?: string) => string | undefined; + remove: (widgetId?: string) => void; +} + +declare global { + interface Window { + turnstile?: TurnstileInstance; + } +} diff --git a/frontend/src/environments/environment.saas-prod.ts b/frontend/src/environments/environment.saas-prod.ts index b01db2531..3810d785f 100644 --- a/frontend/src/environments/environment.saas-prod.ts +++ b/frontend/src/environments/environment.saas-prod.ts @@ -1,9 +1,10 @@ export const environment = { - production: true, - saas: true, - apiRoot: "/api", - saasURL: "", - saasHostnames: ['app.rocketadmin.com', 'localhost', 'rocketadmin-dev.tail9f8b2.ts.net'], - stagingHost: "rocketadmin-dev.tail9f8b2.ts.net", // Tailscale host - version: '0.0.0' - }; + production: true, + saas: true, + apiRoot: '/api', + saasURL: '', + saasHostnames: ['app.rocketadmin.com', 'localhost', 'rocketadmin-dev.tail9f8b2.ts.net'], + stagingHost: 'rocketadmin-dev.tail9f8b2.ts.net', // Tailscale host + version: '0.0.0', + turnstileSiteKey: '0x4AAAAAACM2ZuNYhGhncig_', +}; diff --git a/frontend/src/environments/environment.saas.ts b/frontend/src/environments/environment.saas.ts index c1b1dd0f8..0c61886f2 100644 --- a/frontend/src/environments/environment.saas.ts +++ b/frontend/src/environments/environment.saas.ts @@ -1,9 +1,10 @@ export const environment = { - saas: true, - production: false, - apiRoot: "/api", - saasURL: "", - saasHostnames: ['app.rocketadmin.com', 'localhost', 'rocketadmin-dev.tail9f8b2.ts.net'], - stagingHost: "rocketadmin-dev.tail9f8b2.ts.net", // Tailscale host - version: '0.0.0' + saas: true, + production: false, + apiRoot: 'https://rocketadmin-dev.tail9f8b2.ts.net/api', + saasURL: 'https://rocketadmin-dev.tail9f8b2.ts.net', + saasHostnames: ['app.rocketadmin.com', 'localhost', 'rocketadmin-dev.tail9f8b2.ts.net'], + stagingHost: 'rocketadmin-dev.tail9f8b2.ts.net', // Tailscale host + version: '0.0.0', + turnstileSiteKey: '1x00000000000000000000AA', // Test key - always passes }; diff --git a/frontend/src/index.saas.html b/frontend/src/index.saas.html index 58a8d8b85..294c7f2d0 100644 --- a/frontend/src/index.saas.html +++ b/frontend/src/index.saas.html @@ -152,6 +152,9 @@ } } + + +