;
+ 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 @@
}
}
+
+
+