From ef0bf478938ccbec33227656bac8c6bbb50b8f56 Mon Sep 17 00:00:00 2001 From: LuffyNoNika <236547409+LuffyNoNika@users.noreply.github.com> Date: Tue, 31 Mar 2026 07:44:04 +0000 Subject: [PATCH 1/6] user with no accessGroup read perms can create hashlist --- src/app/core/_services/main.service.ts | 9 +- .../roles/hashlists/hashlist-role.service.ts | 2 +- .../new-files/new-files.component.spec.ts | 52 ++++++++++- .../files/new-files/new-files.component.ts | 92 +++++-------------- .../new-hashlist.component.spec.ts | 45 ++++++++- .../new-hashlist/new-hashlist.component.ts | 36 ++++++-- 6 files changed, 149 insertions(+), 87 deletions(-) diff --git a/src/app/core/_services/main.service.ts b/src/app/core/_services/main.service.ts index c79cb973d..e63b10846 100644 --- a/src/app/core/_services/main.service.ts +++ b/src/app/core/_services/main.service.ts @@ -276,9 +276,14 @@ export class GlobalService { .pipe(debounceTime(2000)); } - getRelationships(serviceConfig: ServiceConfig, id: number, relType: string): Observable { + getRelationships( + serviceConfig: ServiceConfig, + id: number, + relType: string, + options?: { headers?: HttpHeaders } + ): Observable { return this.http - .get(this.cs.getEndpoint() + serviceConfig.URL + '/' + id + '/' + relType) + .get(this.cs.getEndpoint() + serviceConfig.URL + '/' + id + '/' + relType, options) .pipe(debounceTime(2000)); } diff --git a/src/app/core/_services/roles/hashlists/hashlist-role.service.ts b/src/app/core/_services/roles/hashlists/hashlist-role.service.ts index 15f8a6aa2..5eddaea6b 100644 --- a/src/app/core/_services/roles/hashlists/hashlist-role.service.ts +++ b/src/app/core/_services/roles/hashlists/hashlist-role.service.ts @@ -14,7 +14,7 @@ import { RoleService } from '@services/roles/base/role.service'; export class HashListRoleService extends RoleService { constructor(permissionService: PermissionService) { super(permissionService, { - create: [Perm.Hashlist.CREATE, Perm.Hashtype.READ, Perm.GroupAccess.READ], + create: [Perm.Hashlist.CREATE, Perm.Hashtype.READ], read: [Perm.Hashlist.READ], update: [Perm.Hashlist.UPDATE], tasks: [Perm.TaskWrapper.READ], diff --git a/src/app/files/new-files/new-files.component.spec.ts b/src/app/files/new-files/new-files.component.spec.ts index c14bb8097..4bda18a62 100644 --- a/src/app/files/new-files/new-files.component.spec.ts +++ b/src/app/files/new-files/new-files.component.spec.ts @@ -1,5 +1,6 @@ -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; +import { HttpHeaders } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; @@ -205,17 +206,60 @@ describe('NewFilesComponent', () => { }); describe('Access group scoping', () => { - it('should fetch access groups via getRelationships for the current user, not getAll', () => { + it('should fetch access groups via getRelationships with X-Skip-Error-Dialog header', () => { setup('wordlist-new'); const gs = TestBed.inject(GlobalService) as unknown as MockGlobalService; - // Component must use getRelationships to get user-scoped access groups - expect(gs.getRelationships).toHaveBeenCalledWith(SERV.USERS, 1, RelationshipType.ACCESSGROUPS); + const callArgs = gs.getRelationships.calls.mostRecent().args; + expect(callArgs[0]).toEqual(SERV.USERS); + expect(callArgs[1]).toBe(1); + expect(callArgs[2]).toBe(RelationshipType.ACCESSGROUPS); + expect(callArgs[3]).toBeDefined(); + expect(callArgs[3].headers).toBeInstanceOf(HttpHeaders); + expect(callArgs[3].headers.get('X-Skip-Error-Dialog')).toBe('true'); // getAll must NOT be called — the component should not fetch all access groups expect(gs.getAll).not.toHaveBeenCalled(); }); + it('should fall back to default access group when getRelationships returns error (403)', async () => { + TestBed.overrideProvider(GlobalService, { + useValue: { + ...new MockGlobalService(), + getRelationships: jasmine + .createSpy('getRelationships') + .and.returnValue(throwError(() => new Error('403 Forbidden'))), + userId: 1 + } + }); + TestBed.overrideProvider(ActivatedRoute, { + useValue: { data: of({ kind: 'wordlist-new' }) } + }); + + fixture = TestBed.createComponent(NewFilesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.selectAccessgroup.length).toBe(1); + expect(component.selectAccessgroup[0]).toEqual({ id: '1', name: 'Default' }); + expect(component.form.get('accessGroupId').value).toBe(1); + expect(component.form.get('accessGroupId').disabled).toBeTrue(); + expect(component.isLoading).toBeFalse(); + }); + + it('should fall back to default access group when response has empty data', async () => { + // Default mock already returns { data: [], included: [] } → empty → triggers fallback + setup('wordlist-new'); + await fixture.whenStable(); + + expect(component.selectAccessgroup.length).toBe(1); + expect(component.selectAccessgroup[0]).toEqual({ id: '1', name: 'Default' }); + expect(component.form.get('accessGroupId').value).toBe(1); + expect(component.form.get('accessGroupId').disabled).toBeTrue(); + expect(component.isLoading).toBeFalse(); + }); + it('should correctly transform access group API data to select options', () => { // Simulate deserialized access groups (what JsonAPISerializer would produce) const deserialized = [ diff --git a/src/app/files/new-files/new-files.component.ts b/src/app/files/new-files/new-files.component.ts index 740918cfa..a240876b9 100644 --- a/src/app/files/new-files/new-files.component.ts +++ b/src/app/files/new-files/new-files.component.ts @@ -1,7 +1,8 @@ import { Subject, firstValueFrom, takeUntil } from 'rxjs'; +import { HttpHeaders } from '@angular/common/http'; import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core'; -import { FormGroup, Validators } from '@angular/forms'; +import { FormGroup } from '@angular/forms'; import { MatCheckboxChange } from '@angular/material/checkbox'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; @@ -150,27 +151,6 @@ export class NewFilesComponent implements OnInit, OnDestroy { buildForm() { this.form = getNewFilesForm(); this.form.patchValue({ fileType: this.filterType }); - this.updateValidatorsBySourceType(this.form.get('sourceType').value); - } - - private updateValidatorsBySourceType(sourceType: string): void { - const filenameCtrl = this.form.get('filename'); - const urlCtrl = this.form.get('url'); - - if (!filenameCtrl || !urlCtrl) { - return; - } - - if (sourceType === 'url') { - filenameCtrl.setValidators([Validators.required]); - urlCtrl.setValidators([Validators.required]); - } else { - filenameCtrl.clearValidators(); - urlCtrl.clearValidators(); - } - - filenameCtrl.updateValueAndValidity({ emitEvent: false }); - urlCtrl.updateValueAndValidity({ emitEvent: false }); } /** @@ -178,10 +158,11 @@ export class NewFilesComponent implements OnInit, OnDestroy { */ async loadData() { this.isLoading = true; + const skipErrorHeaders = { headers: new HttpHeaders({ 'X-Skip-Error-Dialog': 'true' }) }; try { const response: ResponseWrapper = await firstValueFrom( - this.gs.getRelationships(SERV.USERS, this.gs.userId, RelationshipType.ACCESSGROUPS) + this.gs.getRelationships(SERV.USERS, this.gs.userId, RelationshipType.ACCESSGROUPS, skipErrorHeaders) ); const accessGroups = new JsonAPISerializer().deserialize({ @@ -190,14 +171,23 @@ export class NewFilesComponent implements OnInit, OnDestroy { }); this.selectAccessgroup = transformSelectOptions(accessGroups, ACCESS_GROUP_FIELD_MAPPING); - } catch (error) { - console.error('Error fetching access groups:', error); + if (!this.selectAccessgroup || this.selectAccessgroup.length === 0) { + this.setDefaultAccessGroup(); + } + } catch { + this.setDefaultAccessGroup(); } finally { this.isLoading = false; this.changeDetectorRef.detectChanges(); } } + private setDefaultAccessGroup(): void { + this.selectAccessgroup = [{ id: '1', name: 'Default' }]; + this.form.patchValue({ accessGroupId: 1 }); + this.form.get('accessGroupId').disable(); + } + /** * Loads the list of files available on the server in the import directory. */ @@ -223,7 +213,7 @@ export class NewFilesComponent implements OnInit, OnDestroy { */ async onSubmit(): Promise { if (this.form.valid && !this.submitted) { - const form = this.onBeforeSubmit(this.form.value, false); + const form = this.onBeforeSubmit(this.form.getRawValue(), false); this.isCreatingLoading = true; this.submitted = true; @@ -233,7 +223,7 @@ export class NewFilesComponent implements OnInit, OnDestroy { await firstValueFrom(this.gs.create(SERV.FILES, form.update)); // After successful creation, update form and show alert - this.onBeforeSubmit(this.form.value, true); + this.onBeforeSubmit(this.form.getRawValue(), true); this.alert.showSuccessMessage('New File created'); this.isCreatingLoading = false; this.submitted = false; @@ -299,7 +289,6 @@ export class NewFilesComponent implements OnInit, OnDestroy { sourceType: type, sourceData: '' }); - this.updateValidatorsBySourceType(type); // Load server import directory files only when switching to tab3 if (view === 'tab3' && this.serverFiles.length === 0) { @@ -338,50 +327,22 @@ export class NewFilesComponent implements OnInit, OnDestroy { this.alert.showErrorMessage('Please select a file to upload.'); return; } - const form = this.onBeforeSubmit(this.form.value, false); + const form = this.onBeforeSubmit(this.form.getRawValue(), false); this.isCreatingLoading = true; for (let i = 0; i < files.length; i++) { this.uploadService .uploadFile(files[0], files[0].name, SERV.FILES, form.update, ['/files', this.redirect]) .pipe(takeUntil(this.fileUnsubscribe)) - .subscribe({ - next: (progress) => { - this.uploadProgress = progress; - this.changeDetectorRef.detectChanges(); - if (this.uploadProgress === 100) { - this.isCreatingLoading = false; - } - }, - error: (error) => { - this.uploadProgress = 0; + .subscribe((progress) => { + this.uploadProgress = progress; + this.changeDetectorRef.detectChanges(); + if (this.uploadProgress === 100) { this.isCreatingLoading = false; - this.alert.showErrorMessage(this.buildUploadErrorMessage(error)); - this.changeDetectorRef.detectChanges(); } }); } } - private buildUploadErrorMessage(error: unknown): string { - if (typeof error === 'string') { - return `Failed to upload file: ${error}`; - } - - if (error && typeof error === 'object') { - const errorObject = error as { error?: { title?: string; message?: string }; message?: string }; - const backendMessage = errorObject.error?.title || errorObject.error?.message; - if (backendMessage) { - return `Failed to upload file: ${backendMessage}`; - } - - if (errorObject.message) { - return `Failed to upload file: ${errorObject.message}`; - } - } - - return 'Failed to upload file.'; - } - /** * Handles the submission of selected server files for import. * @protected @@ -389,7 +350,6 @@ export class NewFilesComponent implements OnInit, OnDestroy { protected async onSubmitServerImport() { if (this.selectedServerFiles.size === 0) return; if (this.submitted) return; - if (this.form.invalid) return; this.isCreatingLoading = true; this.submitted = true; @@ -399,9 +359,9 @@ export class NewFilesComponent implements OnInit, OnDestroy { // Build form data for each server file const payload = { filename: file, - isSecret: this.form.value.isSecret || false, + isSecret: this.form.getRawValue().isSecret || false, fileType: this.filterType, - accessGroupId: this.form.value.accessGroupId, + accessGroupId: this.form.getRawValue().accessGroupId, sourceType: 'import', // IMPORTANT: tells backend to pick it from server import dir sourceData: file // some backends require this too }; @@ -414,9 +374,7 @@ export class NewFilesComponent implements OnInit, OnDestroy { await Promise.all(requests); this.alert.showSuccessMessage('Server files imported successfully!'); - // Reload server files - await this.loadServerFiles(); - this.selectedServerFiles.clear(); + void this.router.navigate(['/files', this.redirect]); } catch (error) { console.error('Error importing server files:', error); this.alert.showErrorMessage('Could not import selected files.'); diff --git a/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts b/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts index 38715e627..0f33da641 100644 --- a/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts +++ b/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts @@ -1,5 +1,6 @@ -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; +import { HttpHeaders } from '@angular/common/http'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; @@ -245,7 +246,7 @@ describe('NewHashlistComponent', () => { tick(); const expectedPayload = { - ...component.form.value, + ...component.form.getRawValue(), sourceType: 'import', sourceData: 'hashes.txt' }; @@ -321,9 +322,45 @@ describe('NewHashlistComponent', () => { }); describe('Access group scoping', () => { - it('should fetch access groups via getRelationships for the current user, not getAll', () => { - expect(gsSpy.getRelationships).toHaveBeenCalledWith(SERV.USERS, 1, RelationshipType.ACCESSGROUPS); + it('should fetch access groups via getRelationships with X-Skip-Error-Dialog header', () => { + const callArgs = gsSpy.getRelationships.calls.mostRecent().args; + expect(callArgs[0]).toEqual(SERV.USERS); + expect(callArgs[1]).toBe(1); + expect(callArgs[2]).toBe(RelationshipType.ACCESSGROUPS); + expect(callArgs[3]).toBeDefined(); + expect(callArgs[3].headers).toBeInstanceOf(HttpHeaders); + expect(callArgs[3].headers.get('X-Skip-Error-Dialog')).toBe('true'); + + // getAll must NOT be called for access groups expect(gsSpy.getAll).not.toHaveBeenCalledWith(SERV.ACCESS_GROUPS); }); + + it('should fall back to default access group when getRelationships returns error (403)', () => { + // Re-create component with error response + gsSpy.getRelationships.and.returnValue(throwError(() => new Error('403 Forbidden'))); + fixture = TestBed.createComponent(NewHashlistComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component.selectAccessgroup.length).toBe(1); + expect(component.selectAccessgroup[0]).toEqual({ id: '1', name: 'Default' }); + expect(component.form.get('accessGroupId').value).toBe(1); + expect(component.form.get('accessGroupId').disabled).toBeTrue(); + expect(component.isLoadingAccessGroups).toBeFalse(); + }); + + it('should fall back to default access group when response has empty data', () => { + // Re-create component with empty access groups + gsSpy.getRelationships.and.returnValue(of({ data: [], included: [] })); + fixture = TestBed.createComponent(NewHashlistComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component.selectAccessgroup.length).toBe(1); + expect(component.selectAccessgroup[0]).toEqual({ id: '1', name: 'Default' }); + expect(component.form.get('accessGroupId').value).toBe(1); + expect(component.form.get('accessGroupId').disabled).toBeTrue(); + expect(component.isLoadingAccessGroups).toBeFalse(); + }); }); }); diff --git a/src/app/hashlists/new-hashlist/new-hashlist.component.ts b/src/app/hashlists/new-hashlist/new-hashlist.component.ts index ec1f8a107..4230688a7 100644 --- a/src/app/hashlists/new-hashlist/new-hashlist.component.ts +++ b/src/app/hashlists/new-hashlist/new-hashlist.component.ts @@ -3,6 +3,7 @@ */ import { Subject, Subscription, firstValueFrom, takeUntil } from 'rxjs'; +import { HttpHeaders } from '@angular/common/http'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; @@ -164,16 +165,27 @@ export class NewHashlistComponent implements OnInit, OnDestroy { */ loadData(): void { this.loadConfigs(); + const skipErrorHeaders = { headers: new HttpHeaders({ 'X-Skip-Error-Dialog': 'true' }) }; const accessGroupSubscription = this.gs - .getRelationships(SERV.USERS, this.gs.userId, RelationshipType.ACCESSGROUPS) - .subscribe((response: ResponseWrapper) => { - const accessGroups = new JsonAPISerializer().deserialize({ - data: response.data, - included: response.included - }); - this.selectAccessgroup = transformSelectOptions(accessGroups, ACCESS_GROUP_FIELD_MAPPING); - this.isLoadingAccessGroups = false; - this.changeDetectorRef.detectChanges(); + .getRelationships(SERV.USERS, this.gs.userId, RelationshipType.ACCESSGROUPS, skipErrorHeaders) + .subscribe({ + next: (response: ResponseWrapper) => { + const accessGroups = new JsonAPISerializer().deserialize({ + data: response.data, + included: response.included + }); + this.selectAccessgroup = transformSelectOptions(accessGroups, ACCESS_GROUP_FIELD_MAPPING); + if (!this.selectAccessgroup || this.selectAccessgroup.length === 0) { + this.setDefaultAccessGroup(); + } + this.isLoadingAccessGroups = false; + this.changeDetectorRef.detectChanges(); + }, + error: () => { + this.setDefaultAccessGroup(); + this.isLoadingAccessGroups = false; + this.changeDetectorRef.detectChanges(); + } }); this.unsubscribeService.add(accessGroupSubscription); @@ -189,6 +201,12 @@ export class NewHashlistComponent implements OnInit, OnDestroy { this.unsubscribeService.add(hashtypesSubscription$); } + private setDefaultAccessGroup(): void { + this.selectAccessgroup = [{ id: '1', name: 'Default' }]; + this.form.patchValue({ accessGroupId: 1 }); + this.form.get('accessGroupId').disable(); + } + get sourceType() { return this.form.get('sourceType').value; } From 005a957809c8c73d76ad8d297680769bc9c3c8a3 Mon Sep 17 00:00:00 2001 From: LuffyNoNika <236547409+LuffyNoNika@users.noreply.github.com> Date: Tue, 31 Mar 2026 10:24:35 +0000 Subject: [PATCH 2/6] user with no access group read perm can create an agent --- .../agents/new-agent/new-agent.component.html | 38 +++++++++------ .../new-agent/new-agent.component.spec.ts | 47 ++++++++++++++++++- .../agents/new-agent/new-agent.component.ts | 4 ++ .../roles/agents/agent-role.service.ts | 2 +- 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/src/app/agents/new-agent/new-agent.component.html b/src/app/agents/new-agent/new-agent.component.html index 4643179e5..58be01040 100644 --- a/src/app/agents/new-agent/new-agent.component.html +++ b/src/app/agents/new-agent/new-agent.component.html @@ -2,16 +2,23 @@ - + @if (canReadAgentBinaries) { + + }
-

- Download the agent binary and execute it on the - client server. You can download an agent binary by clicking on the - action menu (...) in the agent binaries table above. -

+ @if (canReadAgentBinaries) { +

+ Download the agent binary and execute it on the + client server. You can download an agent binary by clicking on the + action menu (...) in the agent binaries table above. +

+ } @else { +

+ You do not have permission to view agent binaries. You can skip this step and proceed to register the agent. +

+ }
@@ -53,16 +60,17 @@
-

- Generate a voucher to register the agent. Note that once the voucher - is used it will be automatically deleted. -

- + @if (!allowMultiVoucher) { +

+ Generate a voucher to register the agent. Note that once the voucher + is used it will be automatically deleted. +

+ } @else {

Generate vouchers to register agents. Vouchers remain available when the "Register Multiple Agents Using Voucher(s)" option is enabled.

-
+ }
diff --git a/src/app/agents/new-agent/new-agent.component.spec.ts b/src/app/agents/new-agent/new-agent.component.spec.ts index fae8cf096..8dbd74db7 100644 --- a/src/app/agents/new-agent/new-agent.component.spec.ts +++ b/src/app/agents/new-agent/new-agent.component.spec.ts @@ -1,3 +1,4 @@ +import { Perm } from '@constants/userpermissions.config'; import { of } from 'rxjs'; import { Clipboard } from '@angular/cdk/clipboard'; @@ -13,6 +14,7 @@ import { Router } from '@angular/router'; import { SERV } from '@services/main.config'; import { GlobalService } from '@services/main.service'; +import { PermissionService } from '@services/permission/permission.service'; import { AlertService } from '@services/shared/alert.service'; import { ConfigService } from '@services/shared/config.service'; @@ -28,7 +30,7 @@ import { TableModule } from '@src/app/shared/table/table-actions.module'; standalone: false }) export class MockAgentBinariesTableComponent { - @Input() isSelectable: boolean; + @Input() isSelectable: boolean = false; } // Voucher table mock @@ -48,6 +50,7 @@ describe('NewAgentComponent', () => { let alertServiceSpy: jasmine.SpyObj; let configServiceSpy: jasmine.SpyObj; let globalServiceSpy: jasmine.SpyObj; + let permissionServiceSpy: jasmine.SpyObj; let routerSpy: jasmine.SpyObj; beforeEach(async () => { @@ -55,6 +58,8 @@ describe('NewAgentComponent', () => { alertServiceSpy = jasmine.createSpyObj('AlertService', ['showSuccessMessage']); configServiceSpy = jasmine.createSpyObj('ConfigService', ['getEndpoint']); globalServiceSpy = jasmine.createSpyObj('GlobalService', ['create', 'getAll']); + permissionServiceSpy = jasmine.createSpyObj('PermissionService', ['hasPermissionSync']); + permissionServiceSpy.hasPermissionSync.and.returnValue(true); routerSpy = jasmine.createSpyObj('Router', ['navigate']); // Provide default stub for configService.getEndpoint() @@ -80,6 +85,7 @@ describe('NewAgentComponent', () => { { provide: AlertService, useValue: alertServiceSpy }, { provide: ConfigService, useValue: configServiceSpy }, { provide: GlobalService, useValue: globalServiceSpy }, + { provide: PermissionService, useValue: permissionServiceSpy }, { provide: Router, useValue: routerSpy } ] }).compileComponents(); @@ -175,4 +181,43 @@ describe('NewAgentComponent', () => { component.ngOnDestroy(); expect(component.newVoucherSubscription.unsubscribe).toHaveBeenCalled(); }); + + it('should allow agent creation for a user without AccessGroup.READ permission', fakeAsync(() => { + // Simulate a user who lacks AccessGroup.READ but retains Agent.CREATE, Agent.READ and Voucher.READ + // This is the exact scenario described in issue #1955 + permissionServiceSpy.hasPermissionSync.and.callFake((perm: string) => perm !== Perm.GroupAccess.READ); + + fixture = TestBed.createComponent(NewAgentComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + // Component initializes without errors + expect(component).toBeTruthy(); + + // User can still create a voucher — the actual agent registration action + const voucher = 'fy7vjq56'; + component.form.controls['voucher'].setValue(voucher); + component.table = jasmine.createSpyObj('VouchersTableComponent', ['reload']); + globalServiceSpy.create.and.returnValue(of({})); + + component.onSubmit(); + tick(); + + expect(globalServiceSpy.create).toHaveBeenCalledWith(SERV.VOUCHER, { voucher: voucher }); + expect(alertServiceSpy.showSuccessMessage).toHaveBeenCalledWith('New voucher successfully created!'); + })); + + it('should not make any access group API call during agent creation', () => { + // Agent creation does not require or fetch access groups — verifies no extraneous dependency + const calledEndpoints = globalServiceSpy.getAll.calls.allArgs().map((args: unknown[]) => args[0]); + expect(calledEndpoints).not.toContain(SERV.ACCESS_GROUPS); + }); + + it('should hide agent binaries table and avoid 403 when user lacks AgentBinary.READ permission', () => { + // Secondary fix: users without AgentBinary.READ won't trigger a 403 on the binaries endpoint + component.canReadAgentBinaries = false; + fixture.detectChanges(); + const table = fixture.nativeElement.querySelector('app-agent-binaries-table'); + expect(table).toBeNull(); + }); }); diff --git a/src/app/agents/new-agent/new-agent.component.ts b/src/app/agents/new-agent/new-agent.component.ts index 7ef27ece0..4cad8bc4b 100644 --- a/src/app/agents/new-agent/new-agent.component.ts +++ b/src/app/agents/new-agent/new-agent.component.ts @@ -1,3 +1,4 @@ +import { Perm } from '@constants/userpermissions.config'; import { Subscription } from 'rxjs'; import { VouchersTableComponent } from 'src/app/core/_components/tables/vouchers-table/vouchers-table.component'; import { GlobalService } from 'src/app/core/_services/main.service'; @@ -10,6 +11,7 @@ import { FormControl, FormGroup } from '@angular/forms'; import { Router } from '@angular/router'; import { SERV } from '@services/main.config'; +import { PermissionService } from '@services/permission/permission.service'; import { AlertService } from '@services/shared/alert.service'; import { VoucherForm } from '@src/app/agents/new-agent/new-agent.form'; @@ -30,6 +32,7 @@ export class NewAgentComponent implements OnInit, OnDestroy { private alertService = inject(AlertService); private cs = inject(ConfigService); private gs = inject(GlobalService); + private permissionService = inject(PermissionService); private router = inject(Router); form: FormGroup = new FormGroup({ @@ -38,6 +41,7 @@ export class NewAgentComponent implements OnInit, OnDestroy { agentURL: string; newVoucherSubscription: Subscription; allowMultiVoucher = false; + canReadAgentBinaries = this.permissionService.hasPermissionSync(Perm.AgentBinary.READ); @ViewChild('table') table: VouchersTableComponent; diff --git a/src/app/core/_services/roles/agents/agent-role.service.ts b/src/app/core/_services/roles/agents/agent-role.service.ts index 5f6dc22bf..c94ed2a05 100644 --- a/src/app/core/_services/roles/agents/agent-role.service.ts +++ b/src/app/core/_services/roles/agents/agent-role.service.ts @@ -20,7 +20,7 @@ export class AgentRoleService extends RoleService { readChunk: [Perm.Task.READ, Perm.TaskWrapper.READ, Perm.Chunk.READ], readAccessGroup: [Perm.GroupAccess.READ], readError: [Perm.AgentError.READ], - create: [Perm.Agent.CREATE, Perm.Agent.READ, Perm.Voucher.READ, Perm.AgentBinary.READ], + create: [Perm.Agent.CREATE, Perm.Agent.READ, Perm.Voucher.READ], update: [Perm.Agent.UPDATE], updateAssignment: [Perm.AgentAssignment.UPDATE, Perm.AgentAssignment.READ, Perm.Task.READ, Perm.TaskWrapper.READ] }); From 95af08cdc8a408bdaee324681353e2d6348637af Mon Sep 17 00:00:00 2001 From: LuffyNoNika <236547409+LuffyNoNika@users.noreply.github.com> Date: Tue, 14 Apr 2026 07:50:16 +0000 Subject: [PATCH 3/6] test updated: --- src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts b/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts index 679a84e8b..8d29a1e96 100644 --- a/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts +++ b/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts @@ -355,8 +355,8 @@ describe('NewHashlistComponent', () => { }); it('should fall back to default access group when response has empty data', () => { - // Re-create component with empty access groups - gsSpy.getRelationships.and.returnValue(of({ data: [], included: [] })); + // Re-create component with empty access groups (full ResponseWrapper shape for Zod validation) + gsSpy.getRelationships.and.returnValue(of({ jsonapi: { version: '1.1', ext: [] }, data: [], included: [] })); fixture = TestBed.createComponent(NewHashlistComponent); component = fixture.componentInstance; fixture.detectChanges(); From f28a6a15d7213d4249c9a47448eaa8942ae5e0fc Mon Sep 17 00:00:00 2001 From: LuffyNoNika <236547409+LuffyNoNika@users.noreply.github.com.> Date: Tue, 28 Apr 2026 10:05:24 +0000 Subject: [PATCH 4/6] fix(permissions): fix permission table PATCH, toggle UX and checkbox re-render --- ...ermission-groups-user-table.component.html | 3 +- ...-permission-groups-user-table.component.ts | 124 +++++++++++++++--- .../tables/ht-table/ht-table.component.html | 2 +- .../tables/ht-table/ht-table.component.ts | 16 ++- ...-table-type-editable-checkbox.component.ts | 20 ++- ...ess-permission-groups-expand.datasource.ts | 42 +++--- 6 files changed, 165 insertions(+), 42 deletions(-) diff --git a/src/app/core/_components/tables/access-permission-groups-user-table/access-permission-groups-user-table.component.html b/src/app/core/_components/tables/access-permission-groups-user-table/access-permission-groups-user-table.component.html index ea7e718ed..d908eaa32 100644 --- a/src/app/core/_components/tables/access-permission-groups-user-table/access-permission-groups-user-table.component.html +++ b/src/app/core/_components/tables/access-permission-groups-user-table/access-permission-groups-user-table.component.html @@ -1,4 +1,5 @@ \ No newline at end of file + (editableCheckbox)="onCheckboxChange($event)" (exportActionClicked)="exportActionClicked($event)" + (allToggled)="onAllPermissionsToggled()" (rowToggled)="onRowToggled($event)" /> \ No newline at end of file diff --git a/src/app/core/_components/tables/access-permission-groups-user-table/access-permission-groups-user-table.component.ts b/src/app/core/_components/tables/access-permission-groups-user-table/access-permission-groups-user-table.component.ts index e2bd99948..3ecc65594 100644 --- a/src/app/core/_components/tables/access-permission-groups-user-table/access-permission-groups-user-table.component.ts +++ b/src/app/core/_components/tables/access-permission-groups-user-table/access-permission-groups-user-table.component.ts @@ -1,8 +1,11 @@ import { catchError } from 'rxjs'; -import { AfterViewInit, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { AfterViewInit, Component, Input, OnDestroy, OnInit, inject } from '@angular/core'; +import { DynamicModel } from '@models/base.model'; import { UserPermissions } from '@models/global-permission-group.model'; +import { JUser } from '@models/user.model'; import { SERV } from '@services/main.config'; @@ -28,6 +31,8 @@ export class AccessPermissionGroupsUserTableComponent { @Input() accesspermgroupId = 0; + private http = inject(HttpClient); + tableColumns: HTTableColumn[] = []; dataSource: AccessPermissionGroupsExpandDataSource; expand = 'userMembers'; @@ -120,9 +125,9 @@ export class AccessPermissionGroupsUserTableComponent } // --- Action functions --- - exportActionClicked(event: ActionMenuEvent): void { + exportActionClicked(event: ActionMenuEvent<(JUser | UserPermissions)[]>): void { this.exportService.handleExportAction( - event, + event as ActionMenuEvent, this.tableColumns, AccessPermissionGroupsUserTableColumnLabel, 'hashtopolis-access-permission-groups-user' @@ -133,8 +138,8 @@ export class AccessPermissionGroupsUserTableComponent * Update Permissions on checkbox change event * @param editable Editable object containing current permission, action and changed value */ - onCheckboxChange(editable: HTTableEditable): void { - this.changePermision(editable, editable.value); + onCheckboxChange(editable: HTTableEditable): void { + this.changePermision(editable as HTTableEditable, editable.value); } /** @@ -146,27 +151,112 @@ export class AccessPermissionGroupsUserTableComponent const capitalizedPerm = (editable['action'].match(/-(.*?)-/)?.[1] || '') .toLowerCase() .replace(/^\w/, (c) => c.toUpperCase()); - const keyPerm = editable['data']['originalName'] + capitalizedPerm; + const keyPerm = 'perm' + String((editable.data as unknown as DynamicModel)['key']) + capitalizedPerm; const boolValue = value === 'true' ? true : value === 'false' ? false : Boolean(value); - // Payload - const payload = { - permissions: { [keyPerm]: boolValue } - }; - const request$ = this.gs.update(SERV.ACCESS_PERMISSIONS_GROUPS, this.accesspermgroupId, payload); + // Build the full permissions object from current datasource state, then apply the change + const allPermissions: Record = {}; + (this.dataSource.currentData as UserPermissions[]).forEach((perm) => { + allPermissions[`perm${perm.key}Create`] = perm.create; + allPermissions[`perm${perm.key}Read`] = perm.read; + allPermissions[`perm${perm.key}Update`] = perm.update; + allPermissions[`perm${perm.key}Delete`] = perm.delete; + }); + allPermissions[keyPerm] = boolValue; + + this.patchPermissions( + allPermissions, + `Changed permission in ${capitalizedPerm} on Permission Group #${this.accesspermgroupId}!` + ); + } + + /** + * Grants or revokes all 4 permissions (create/read/update/delete) for a single resource row. + * Direction is derived from the row's current data: if any permission is already true, + * revoke all; otherwise grant all. This avoids relying on the selection checkbox state + * which resets to unchecked after every reload. + */ + onRowToggled(event: { row: JUser | UserPermissions; checked: boolean }): void { + const perm = event.row as UserPermissions; + if (!('key' in perm) || !perm.key) return; + + const allGranted = perm.create && perm.read && perm.update && perm.delete; + const newValue = !allGranted; + + const allPermissions: Record = {}; + (this.dataSource.currentData as UserPermissions[]).forEach((p) => { + allPermissions[`perm${p.key}Create`] = p.create; + allPermissions[`perm${p.key}Read`] = p.read; + allPermissions[`perm${p.key}Update`] = p.update; + allPermissions[`perm${p.key}Delete`] = p.delete; + }); + allPermissions[`perm${perm.key}Create`] = newValue; + allPermissions[`perm${perm.key}Read`] = newValue; + allPermissions[`perm${perm.key}Update`] = newValue; + allPermissions[`perm${perm.key}Delete`] = newValue; + + const label = newValue + ? `Granted all permissions for ${perm.name} on Permission Group #${this.accesspermgroupId}` + : `Revoked all permissions for ${perm.name} on Permission Group #${this.accesspermgroupId}`; + this.patchPermissions(allPermissions, label); + } + + /** + * Grants or revokes all permissions for all resources at once. + * Direction is determined by the current data: if more than half the individual + * permission booleans are already true, the next click revokes; otherwise it grants. + * This correctly handles server-enforced always-false permissions (e.g. AgentError Create/Update) + * that prevent a strict "all === true" check from ever being satisfied. + */ + onAllPermissionsToggled(): void { + const currentPerms = this.dataSource.currentData as UserPermissions[]; + if (currentPerms.length === 0) return; + + const totalPerms = currentPerms.length * 4; + const grantedCount = currentPerms.reduce( + (count, p) => count + (p.create ? 1 : 0) + (p.read ? 1 : 0) + (p.update ? 1 : 0) + (p.delete ? 1 : 0), + 0 + ); + const newValue = grantedCount / totalPerms <= 0.5; // grant when ≤50% granted, revoke when >50% + + const allPermissions: Record = {}; + currentPerms.forEach((perm) => { + allPermissions[`perm${perm.key}Create`] = newValue; + allPermissions[`perm${perm.key}Read`] = newValue; + allPermissions[`perm${perm.key}Update`] = newValue; + allPermissions[`perm${perm.key}Delete`] = newValue; + }); + const label = newValue + ? `Granted all permissions on Permission Group #${this.accesspermgroupId}` + : `Revoked all permissions on Permission Group #${this.accesspermgroupId}`; + this.patchPermissions(allPermissions, label); + } + + /** + * Sends a PATCH to update the global permission group's permissions object. + * Uses a direct HTTP call with the exact JSON:API type the server expects. + */ + private patchPermissions(permissions: Record, successMessage: string): void { + const url = `${this.cs.getEndpoint()}${SERV.ACCESS_PERMISSIONS_GROUPS.URL}/${this.accesspermgroupId}`; + const body = { + data: { + type: 'globalPermissionGroup', + id: this.accesspermgroupId, + attributes: { permissions } + } + }; this.subscriptions.push( - request$ + this.http + .patch(url, body) .pipe( catchError((error) => { - this.alertService.showErrorMessage(`Failed to update permission!`); - console.error('Failed to update permission:', error); + this.alertService.showErrorMessage(`Failed to update permissions!`); + console.error('Failed to update permissions:', error); return []; }) ) .subscribe(() => { - this.alertService.showSuccessMessage( - `Changed permission in ${capitalizedPerm} on Permission Group #${this.accesspermgroupId}!` - ); + this.alertService.showSuccessMessage(successMessage); this.reload(); }) ); diff --git a/src/app/core/_components/tables/ht-table/ht-table.component.html b/src/app/core/_components/tables/ht-table/ht-table.component.html index 618f52918..efa262074 100644 --- a/src/app/core/_components/tables/ht-table/ht-table.component.html +++ b/src/app/core/_components/tables/ht-table/ht-table.component.html @@ -135,7 +135,7 @@ 'padding-left': (row.tasks && row.tasks.length > 0 && row.tasks[0].color) || row.color ? '8px' : '16px' }"> @if (!isCmdTask) { - + } @else { diff --git a/src/app/core/_components/tables/ht-table/ht-table.component.ts b/src/app/core/_components/tables/ht-table/ht-table.component.ts index 9d7ea518e..d39a36c84 100644 --- a/src/app/core/_components/tables/ht-table/ht-table.component.ts +++ b/src/app/core/_components/tables/ht-table/ht-table.component.ts @@ -220,6 +220,12 @@ export class HTTableComponent implements OnInit, AfterViewInit, OnDestroy { /** Fetches user customizations */ @Output() backendSqlFilter: EventEmitter = new EventEmitter(); + /** Emits true when all rows become selected, false when all become deselected via the header checkbox */ + @Output() allToggled = new EventEmitter(); + + /** Emits when a single row checkbox is toggled, with the row data and new checked state */ + @Output() rowToggled = new EventEmitter<{ row: any; checked: boolean }>(); + private uiSettings: UISettingsUtilityClass; private subscriptions: Subscription = new Subscription(); @@ -545,6 +551,8 @@ export class HTTableComponent implements OnInit, AfterViewInit, OnDestroy { */ toggleAll(): void { this.dataSource.toggleAll(); + this.cd.markForCheck(); + this.allToggled.emit(this.dataSource.isAllSelected()); } /** @@ -566,9 +574,13 @@ export class HTTableComponent implements OnInit, AfterViewInit, OnDestroy { * * @param row - The row to toggle. */ - toggleSelect(row: any): void { + toggleSelect(row: any, checked?: boolean): void { if (this.isSelectable) { this.dataSource.toggleRow(row); + this.cd.markForCheck(); + if (checked !== undefined) { + this.rowToggled.emit({ row, checked }); + } } } @@ -657,7 +669,7 @@ export class HTTableComponent implements OnInit, AfterViewInit, OnDestroy { if (!this.isDetailPage) { this.uiSettings.updateTableSettings(this.name, { start: pageAfter, - before: pageBefore, + before: pageBefore ?? undefined, page: event.pageSize, // Store the new page size index: index //store the new table index }); diff --git a/src/app/core/_components/tables/ht-table/type/editable/editable-checkbox/ht-table-type-editable-checkbox.component.ts b/src/app/core/_components/tables/ht-table/type/editable/editable-checkbox/ht-table-type-editable-checkbox.component.ts index 492589ee8..1693bce8d 100644 --- a/src/app/core/_components/tables/ht-table/type/editable/editable-checkbox/ht-table-type-editable-checkbox.component.ts +++ b/src/app/core/_components/tables/ht-table/type/editable/editable-checkbox/ht-table-type-editable-checkbox.component.ts @@ -3,8 +3,10 @@ import { Component, EventEmitter, Input, + OnChanges, OnInit, - Output + Output, + SimpleChanges } from '@angular/core'; import { HTTableColumn, HTTableEditable } from '../../../ht-table.models'; @@ -14,7 +16,7 @@ import { HTTableColumn, HTTableEditable } from '../../../ht-table.models'; changeDetection: ChangeDetectionStrategy.OnPush, standalone: false }) -export class HTTableTypeEditableCheckboxComponent implements OnInit { +export class HTTableTypeEditableCheckboxComponent implements OnInit, OnChanges { checkbox: HTTableEditable; original: string; @@ -27,7 +29,17 @@ export class HTTableTypeEditableCheckboxComponent implements OnInit { editMode = false; ngOnInit(): void { - if (this.tableColumn.checkbox) { + this.initCheckbox(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['element'] && !changes['element'].firstChange) { + this.initCheckbox(); + } + } + + private initCheckbox(): void { + if (this.tableColumn?.checkbox) { this.checkbox = this.tableColumn.checkbox(this.element); this.original = this.checkbox.value; } @@ -38,7 +50,7 @@ export class HTTableTypeEditableCheckboxComponent implements OnInit { } onEditableInputSaved(checked: boolean): void { - event.stopPropagation(); + event?.stopPropagation(); this.checkbox.value = checked.toString(); this.editableCheckboxSaved.emit(this.checkbox); this.editMode = false; diff --git a/src/app/core/_datasources/access-permission-groups-expand.datasource.ts b/src/app/core/_datasources/access-permission-groups-expand.datasource.ts index 67b66f66f..084c7af70 100644 --- a/src/app/core/_datasources/access-permission-groups-expand.datasource.ts +++ b/src/app/core/_datasources/access-permission-groups-expand.datasource.ts @@ -1,5 +1,5 @@ import { zGlobalPermissionGroupResponse } from '@generated/api/zod'; -import { catchError, finalize, of } from 'rxjs'; +import { EMPTY, catchError, finalize } from 'rxjs'; import { JGlobalPermissionGroup, UserPermissions } from '@models/global-permission-group.model'; import { ResponseWrapper } from '@models/response.model'; @@ -15,16 +15,20 @@ export class AccessPermissionGroupsExpandDataSource extends BaseDataSource of([])), + catchError(() => EMPTY), finalize(() => (this.loading = false)) ) .subscribe((response: ResponseWrapper) => { - const globalPermissionGroup: JGlobalPermissionGroup = this.serializer.deserialize( - response, - zGlobalPermissionGroupResponse, - { include: ['userMembers'] as const } - ); + const globalPermissionGroup = this.serializer.deserialize(response, zGlobalPermissionGroupResponse, { + include: ['userMembers'] as const + }); let data: (UserPermissions | JUser)[]; if (this._perm) { data = this.processPermissions(globalPermissionGroup); @@ -62,20 +64,26 @@ export class AccessPermissionGroupsExpandDataSource extends BaseDataSource { + let permId = 0; + return Object.entries(globalPermissionGroup.permissions).reduce((acc, [key, value]) => { const operation = key.replace(/^perm/, '').replace(/(Create|Delete|Read|Update)$/, ''); let operationName = operation.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase(); operationName = operationName.charAt(0).toUpperCase() + operationName.slice(1); const type = key.match(/(Create|Delete|Read|Update)$/)?.[0]; const existingPermission = acc.find((item) => item.name === operationName && item.key === operation); - if (existingPermission) { - existingPermission[type.toLowerCase()] = value; + if (existingPermission && type) { + existingPermission[type.toLowerCase() as 'create' | 'read' | 'update' | 'delete'] = value; } else { - const newPermission = { + const newPermission: UserPermissions = { + id: permId++, + type: 'userPermission', name: operationName, key: operation, - originalName: 'perm' + operation, - [type ? type.toLowerCase() : '']: value + create: false, + read: false, + update: false, + delete: false, + ...(type ? { [type.toLowerCase()]: value } : {}) }; acc.push(newPermission); } From 1eb77055050c9841a1f0060101f3a52acf06038c Mon Sep 17 00:00:00 2001 From: LuffyNoNika <236547409+LuffyNoNika@users.noreply.github.com.> Date: Tue, 28 Apr 2026 11:04:35 +0000 Subject: [PATCH 5/6] fixed type assignment errors --- src/app/files/new-files/new-files.component.ts | 4 ++-- src/app/hashlists/new-hashlist/new-hashlist.component.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/files/new-files/new-files.component.ts b/src/app/files/new-files/new-files.component.ts index 9e620c9d4..ed21f6c2e 100644 --- a/src/app/files/new-files/new-files.component.ts +++ b/src/app/files/new-files/new-files.component.ts @@ -199,9 +199,9 @@ export class NewFilesComponent implements OnInit, OnDestroy { } private setDefaultAccessGroup(): void { - this.selectAccessgroup = [{ id: '1', name: 'Default' }]; + this.selectAccessgroup = [{ id: 1, name: 'Default' }]; this.form.patchValue({ accessGroupId: 1 }); - this.form.get('accessGroupId').disable(); + this.form.get('accessGroupId')?.disable(); } /** diff --git a/src/app/hashlists/new-hashlist/new-hashlist.component.ts b/src/app/hashlists/new-hashlist/new-hashlist.component.ts index 3ac2238f1..7b0d323ea 100644 --- a/src/app/hashlists/new-hashlist/new-hashlist.component.ts +++ b/src/app/hashlists/new-hashlist/new-hashlist.component.ts @@ -198,9 +198,9 @@ export class NewHashlistComponent implements OnInit, OnDestroy { } private setDefaultAccessGroup(): void { - this.selectAccessgroup = [{ id: '1', name: 'Default' }]; + this.selectAccessgroup = [{ id: 1, name: 'Default' }]; this.form.patchValue({ accessGroupId: 1 }); - this.form.get('accessGroupId').disable(); + this.form.get('accessGroupId')?.disable(); } get sourceType() { From d61208639ee3be9f03d893e013a81d60c3528533 Mon Sep 17 00:00:00 2001 From: LuffyNoNika <236547409+LuffyNoNika@users.noreply.github.com.> Date: Tue, 28 Apr 2026 12:15:23 +0000 Subject: [PATCH 6/6] fix strict-mode TS errors in specs (id type, non-null assertions, Observable casts) Co-authored-by: Copilot --- .../new-agent/new-agent.component.spec.ts | 2 +- .../tables/ht-table/ht-table.component.ts | 2 +- .../new-files/new-files.component.spec.ts | 12 +++++----- .../new-hashlist.component.spec.ts | 22 ++++++++++--------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/app/agents/new-agent/new-agent.component.spec.ts b/src/app/agents/new-agent/new-agent.component.spec.ts index b3d7d679b..8c02951aa 100644 --- a/src/app/agents/new-agent/new-agent.component.spec.ts +++ b/src/app/agents/new-agent/new-agent.component.spec.ts @@ -199,7 +199,7 @@ describe('NewAgentComponent', () => { const voucher = 'fy7vjq56'; component.form.controls['voucher'].setValue(voucher); component.table = jasmine.createSpyObj('VouchersTableComponent', ['reload']); - globalServiceSpy.create.and.returnValue(of({})); + globalServiceSpy.create.and.returnValue(of({} as any)); component.onSubmit(); tick(); diff --git a/src/app/core/_components/tables/ht-table/ht-table.component.ts b/src/app/core/_components/tables/ht-table/ht-table.component.ts index b4a3ebe58..4534cf0cb 100644 --- a/src/app/core/_components/tables/ht-table/ht-table.component.ts +++ b/src/app/core/_components/tables/ht-table/ht-table.component.ts @@ -220,7 +220,7 @@ export class HTTableComponent implements OnInit, AfterViewI @Output() allToggled = new EventEmitter(); /** Emits when a single row checkbox is toggled, with the row data and new checked state */ - @Output() rowToggled = new EventEmitter<{ row: any; checked: boolean }>(); + @Output() rowToggled = new EventEmitter<{ row: T; checked: boolean }>(); private uiSettings: UISettingsUtilityClass; private subscriptions: Subscription = new Subscription(); diff --git a/src/app/files/new-files/new-files.component.spec.ts b/src/app/files/new-files/new-files.component.spec.ts index 1b840c68f..28465365d 100644 --- a/src/app/files/new-files/new-files.component.spec.ts +++ b/src/app/files/new-files/new-files.component.spec.ts @@ -245,9 +245,9 @@ describe('NewFilesComponent', () => { await fixture.whenStable(); expect(component.selectAccessgroup.length).toBe(1); - expect(component.selectAccessgroup[0]).toEqual({ id: '1', name: 'Default' }); - expect(component.form.get('accessGroupId').value).toBe(1); - expect(component.form.get('accessGroupId').disabled).toBeTrue(); + expect(component.selectAccessgroup[0]).toEqual({ id: 1, name: 'Default' }); + expect(component.form.get('accessGroupId')!.value).toBe(1); + expect(component.form.get('accessGroupId')!.disabled).toBeTrue(); expect(component.isLoading).toBeFalse(); }); @@ -257,9 +257,9 @@ describe('NewFilesComponent', () => { await fixture.whenStable(); expect(component.selectAccessgroup.length).toBe(1); - expect(component.selectAccessgroup[0]).toEqual({ id: '1', name: 'Default' }); - expect(component.form.get('accessGroupId').value).toBe(1); - expect(component.form.get('accessGroupId').disabled).toBeTrue(); + expect(component.selectAccessgroup[0]).toEqual({ id: 1, name: 'Default' }); + expect(component.form.get('accessGroupId')!.value).toBe(1); + expect(component.form.get('accessGroupId')!.disabled).toBeTrue(); expect(component.isLoading).toBeFalse(); }); diff --git a/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts b/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts index b64fda51f..a81fc6e93 100644 --- a/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts +++ b/src/app/hashlists/new-hashlist/new-hashlist.component.spec.ts @@ -327,9 +327,9 @@ describe('NewHashlistComponent', () => { expect(callArgs[0]).toEqual(SERV.USERS); expect(callArgs[1]).toBe(1); expect(callArgs[2]).toBe(RelationshipType.ACCESSGROUPS); - expect(callArgs[3]).toBeDefined(); - expect(callArgs[3].headers).toBeInstanceOf(HttpHeaders); - expect(callArgs[3].headers.get('X-Skip-Error-Dialog')).toBe('true'); + expect(callArgs[3]!).toBeDefined(); + expect(callArgs[3]!.headers).toBeInstanceOf(HttpHeaders); + expect(callArgs[3]!.headers!.get('X-Skip-Error-Dialog')).toBe('true'); // getAll must NOT be called for access groups expect(gsSpy.getAll).not.toHaveBeenCalledWith(SERV.ACCESS_GROUPS); @@ -343,23 +343,25 @@ describe('NewHashlistComponent', () => { fixture.detectChanges(); expect(component.selectAccessgroup.length).toBe(1); - expect(component.selectAccessgroup[0]).toEqual({ id: '1', name: 'Default' }); - expect(component.form.get('accessGroupId').value).toBe(1); - expect(component.form.get('accessGroupId').disabled).toBeTrue(); + expect(component.selectAccessgroup[0]).toEqual({ id: 1, name: 'Default' }); + expect(component.form.get('accessGroupId')!.value).toBe(1); + expect(component.form.get('accessGroupId')!.disabled).toBeTrue(); expect(component.isLoadingAccessGroups).toBeFalse(); }); it('should fall back to default access group when response has empty data', () => { // Re-create component with empty access groups (full ResponseWrapper shape for Zod validation) - gsSpy.getRelationships.and.returnValue(of({ jsonapi: { version: '1.1', ext: [] }, data: [], included: [] })); + gsSpy.getRelationships.and.returnValue( + of({ jsonapi: { version: '1.1', ext: [] }, data: [], included: [] } as any) + ); fixture = TestBed.createComponent(NewHashlistComponent); component = fixture.componentInstance; fixture.detectChanges(); expect(component.selectAccessgroup.length).toBe(1); - expect(component.selectAccessgroup[0]).toEqual({ id: '1', name: 'Default' }); - expect(component.form.get('accessGroupId').value).toBe(1); - expect(component.form.get('accessGroupId').disabled).toBeTrue(); + expect(component.selectAccessgroup[0]).toEqual({ id: 1, name: 'Default' }); + expect(component.form.get('accessGroupId')!.value).toBe(1); + expect(component.form.get('accessGroupId')!.disabled).toBeTrue(); expect(component.isLoadingAccessGroups).toBeFalse(); }); });