diff --git a/src/app/tasks/import-supertasks/masks/masks.component.spec.ts b/src/app/tasks/import-supertasks/masks/masks.component.spec.ts new file mode 100644 index 00000000..ef85ba43 --- /dev/null +++ b/src/app/tasks/import-supertasks/masks/masks.component.spec.ts @@ -0,0 +1,337 @@ +import { of, throwError } from 'rxjs'; + +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { SERV } from '@services/main.config'; +import { GlobalService } from '@services/main.service'; +import { AlertService } from '@services/shared/alert.service'; +import { AutoTitleService } from '@services/shared/autotitle.service'; +import { UnsubscribeService } from '@services/unsubscribe.service'; + +import { MasksComponent } from '@src/app/tasks/import-supertasks/masks/masks.component'; + +const MOCK_CRACKER_TYPES_RESPONSE = { + data: [{ id: '1', type: 'CrackerTypes', attributes: { typeName: 'hashcat' } }], + included: [] +}; + +describe('MasksComponent', () => { + let component: MasksComponent; + let fixture: ComponentFixture; + + let globalServiceSpy: jasmine.SpyObj; + let alertServiceSpy: jasmine.SpyObj; + let routerSpy: jasmine.SpyObj; + + beforeEach(async () => { + globalServiceSpy = jasmine.createSpyObj('GlobalService', ['getAll', 'chelper']); + globalServiceSpy.getAll.and.returnValue(of(MOCK_CRACKER_TYPES_RESPONSE)); + globalServiceSpy.chelper.and.returnValue(of({})); + + alertServiceSpy = jasmine.createSpyObj('AlertService', ['showSuccessMessage', 'showErrorMessage']); + routerSpy = jasmine.createSpyObj('Router', ['navigate']); + routerSpy.navigate.and.returnValue(Promise.resolve(true)); + + await TestBed.configureTestingModule({ + declarations: [MasksComponent], + providers: [ + { provide: AutoTitleService, useValue: jasmine.createSpyObj('AutoTitleService', ['set']) }, + { provide: GlobalService, useValue: globalServiceSpy }, + { provide: AlertService, useValue: alertServiceSpy }, + { provide: Router, useValue: routerSpy }, + { provide: UnsubscribeService, useValue: jasmine.createSpyObj('UnsubscribeService', ['add', 'unsubscribeAll']) } + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + .overrideComponent(MasksComponent, { set: { template: '' } }) + .compileComponents(); + + fixture = TestBed.createComponent(MasksComponent); + component = fixture.componentInstance; + }); + + // ────────────────────────────────────────────── + // Component creation + // ────────────────────────────────────────────── + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + // ────────────────────────────────────────────── + // Form initialization + // ────────────────────────────────────────────── + + it('should have an invalid form by default (name and masks required)', () => { + expect(component.createForm.valid).toBe(false); + }); + + it('should have default form values', () => { + const val = component.createForm.value; + expect(val.maxAgents).toBe(0); + expect(val.isSmall).toBe(false); + expect(val.isCpuTask).toBe(false); + expect(val.optFlag).toBe(false); + expect(val.useNewBench).toBe(true); + expect(val.crackerBinaryId).toBe(1); + }); + + it('should become valid when name and masks are filled', () => { + component.createForm.patchValue({ name: 'Test', masks: '?a?a?a' }); + expect(component.createForm.valid).toBe(true); + }); + + // ────────────────────────────────────────────── + // onSubmit — invalid form + // ────────────────────────────────────────────── + + it('should not call chelper when form is invalid', () => { + component.onSubmit(); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should mark form as touched when submitting invalid form', () => { + component.onSubmit(); + expect(component.createForm.touched).toBe(true); + }); + + // ────────────────────────────────────────────── + // onSubmit — simple mask (no custom charsets) + // ────────────────────────────────────────────── + + it('should call chelper with maskSupertaskBuilder for a simple mask', () => { + component.createForm.patchValue({ name: 'Simple', masks: '?a?a?a?a' }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ + name: 'Simple', + masks: '?a?a?a?a' + }) + ); + }); + + // ────────────────────────────────────────────── + // onSubmit — mask with custom charsets (hcmask format) + // ────────────────────────────────────────────── + + it('should pass hcmask lines with custom charsets as-is to the backend', () => { + const hcmask = '?u?s,?l?d,?1?2?1?2?1?2'; + component.createForm.patchValue({ name: 'Custom Charset', masks: hcmask }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ + name: 'Custom Charset', + masks: hcmask + }) + ); + }); + + it('should pass multiple hcmask lines (multiline) as-is to the backend', () => { + const masks = '?u?s,?l?d,?1?2?1?2?1?2\n?a?a?a?a\n?d?d?d?d?d?d'; + component.createForm.patchValue({ name: 'Multi', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass masks with all four custom charsets to the backend', () => { + const masks = 'abc,def,ghi,jkl,?1?2?3?4'; + component.createForm.patchValue({ name: 'Four charsets', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass masks with commas in charset definitions (e.g. hex) to the backend', () => { + const masks = '0123456789abcdef,,?1?1?1?1?1?1'; + component.createForm.patchValue({ name: 'Hex', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + // ────────────────────────────────────────────── + // onSubmit — payload field mapping + // ────────────────────────────────────────────── + + it('should map form fields to correct payload keys', () => { + component.createForm.patchValue({ + name: 'Mapped', + masks: '?d?d?d', + isCpuTask: true, + isSmall: true, + optFlag: true, + useNewBench: true, + crackerBinaryId: 5, + maxAgents: 3 + }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith(SERV.HELPER, 'maskSupertaskBuilder', { + name: 'Mapped', + masks: '?d?d?d', + isCpu: true, + isSmall: true, + optimized: true, + crackerBinaryTypeId: 5, + benchtype: 'speed', + maxAgents: 3 + }); + }); + + it('should set benchtype to "runtime" when useNewBench is false', () => { + component.createForm.patchValue({ + name: 'Runtime', + masks: '?d?d', + useNewBench: false + }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ benchtype: 'runtime' }) + ); + }); + + it('should set benchtype to "speed" when useNewBench is true', () => { + component.createForm.patchValue({ + name: 'Speed', + masks: '?d?d', + useNewBench: true + }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ benchtype: 'speed' }) + ); + }); + + // ────────────────────────────────────────────── + // onSubmit — success flow + // ────────────────────────────────────────────── + + it('should show success message and navigate on success', () => { + component.createForm.patchValue({ name: 'OK', masks: '?a' }); + component.onSubmit(); + + expect(alertServiceSpy.showSuccessMessage).toHaveBeenCalledWith('New Supertask Mask created'); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/tasks/supertasks']); + }); + + it('should set isLoading true during submission', () => { + component.createForm.patchValue({ name: 'Load', masks: '?a' }); + // Before submit + expect(component.isLoading).toBe(false); + component.onSubmit(); + // After complete callback, isLoading should be false again + expect(component.isLoading).toBe(false); + }); + + // ────────────────────────────────────────────── + // onSubmit — error flow + // ────────────────────────────────────────────── + + it('should reset isLoading on chelper error', () => { + globalServiceSpy.chelper.and.returnValue(throwError(() => new Error('Server error'))); + component.createForm.patchValue({ name: 'Err', masks: '?a' }); + component.onSubmit(); + + expect(component.isLoading).toBe(false); + }); + + it('should not navigate on chelper error', () => { + globalServiceSpy.chelper.and.returnValue(throwError(() => new Error('Server error'))); + component.createForm.patchValue({ name: 'Err', masks: '?a' }); + component.onSubmit(); + + expect(routerSpy.navigate).not.toHaveBeenCalled(); + }); + + // ────────────────────────────────────────────── + // Edge cases — mask content variations + // ────────────────────────────────────────────── + + it('should pass empty-charset hcmask lines (consecutive commas) to the backend', () => { + // e.g. charset1 defined, charset2 empty, mask references ?1 + const masks = 'abc,,,?1?1?1'; + component.createForm.patchValue({ name: 'Empty charset', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass masks with mixed simple and hcmask lines to the backend', () => { + const masks = '?a?a?a\n?u?l,?1?1?1?1\n?d?d?d?d?d?d'; + component.createForm.patchValue({ name: 'Mixed', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass masks with only built-in charsets (?l?u?d?s?a?b?h?H) to the backend', () => { + const masks = '?l?u?d?s?a?b'; + component.createForm.patchValue({ name: 'Builtins', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass masks with Windows-style line endings to the backend', () => { + const masks = '?a?a\r\n?d?d?d'; + component.createForm.patchValue({ name: 'CRLF', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); + + it('should pass a single long mask to the backend', () => { + const masks = '?a'.repeat(50); // 50 positions + component.createForm.patchValue({ name: 'Long mask', masks }); + component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'maskSupertaskBuilder', + jasmine.objectContaining({ masks }) + ); + }); +}); diff --git a/src/app/tasks/import-supertasks/masks/masks.component.ts b/src/app/tasks/import-supertasks/masks/masks.component.ts index 09699c4e..c7393bd7 100644 --- a/src/app/tasks/import-supertasks/masks/masks.component.ts +++ b/src/app/tasks/import-supertasks/masks/masks.component.ts @@ -1,18 +1,16 @@ import { CRACKER_TYPE_FIELD_MAPPING } from '@constants/select.config'; import { benchmarkType } from '@constants/tasks.config'; -import { ChangeDetectorRef, Component, OnDestroy, OnInit, inject } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { HorizontalNav } from '@models/horizontalnav.model'; -import { JPretask } from '@models/pretask.model'; import { SERV } from '@services/main.config'; import { GlobalService } from '@services/main.service'; import { AlertService } from '@services/shared/alert.service'; import { AutoTitleService } from '@services/shared/autotitle.service'; -import { UIConfigService } from '@services/shared/storage.service'; import { UnsubscribeService } from '@services/unsubscribe.service'; import { JCrackerBinaryType } from '@src/app/core/_models/cracker-binary.model'; @@ -41,9 +39,7 @@ export class MasksComponent implements OnInit, OnDestroy { */ private unsubscribeService = inject(UnsubscribeService); - private changeDetectorRef = inject(ChangeDetectorRef); private titleService = inject(AutoTitleService); - private uiService = inject(UIConfigService); private alert = inject(AlertService); private gs = inject(GlobalService); private router = inject(Router); @@ -129,107 +125,42 @@ export class MasksComponent implements OnInit, OnDestroy { } /** - * Create Pretasks - * Name: first line of mask - * Attack: #HL# -a 3 {mask} {options} - * Options: Flag -O (Optimize) + * Handles the submission of the form to create a new super task via the backend helper. + * The backend handles all hcmask parsing, pretask creation and supertask creation. */ - private async preTasks(form): Promise { - return new Promise((resolve, reject) => { - const preTasksIds: number[] = []; - - // Split masks from form.masks by line break and create an array to iterate - const masksArray: string[] = form.masks.split('\n'); - - // Create an array to hold all subscription promises - const subscriptionPromises: Promise[] = []; - - // Iterate over the masks array - masksArray.forEach((maskline, index) => { - let attackCmdSuffix = ''; - if (form.optFlag) { - attackCmdSuffix = '-O'; + onSubmit(): void { + if (this.createForm.valid) { + this.isLoading = true; + const form = this.createForm.value; + const payload = { + name: form.name, + masks: form.masks, + isCpu: form.isCpuTask, + isSmall: form.isSmall, + optimized: form.optFlag, + crackerBinaryTypeId: form.crackerBinaryId, + benchtype: form.useNewBench ? 'speed' : 'runtime', + maxAgents: form.maxAgents + }; + + const subscription$ = this.gs.chelper(SERV.HELPER, 'maskSupertaskBuilder', payload).subscribe({ + next: () => { + this.alert.showSuccessMessage('New Supertask Mask created'); + this.router.navigate(['/tasks/supertasks']); + }, + error: (error) => { + console.error('Error creating mask supertask:', error); + this.isLoading = false; + }, + complete: () => { + this.isLoading = false; } - const payload = { - taskName: maskline, - attackCmd: `#HL# -a 3 ${maskline} ${attackCmdSuffix}`, - maxAgents: form.maxAgents, - chunkTime: this.uiService.getUISettings()?.chunktime ?? 0, - statusTimer: this.uiService.getUISettings()?.statustimer ?? 0, - priority: index + 1, - color: '', - isCpuTask: form.isCpuTask, - crackerBinaryTypeId: form.crackerBinaryId, - isSmall: form.isSmall, - useNewBench: form.useNewBench, - isMaskImport: true, - files: [] - }; - - // Create a subscription promise and push it to the array - const subscriptionPromise = new Promise((resolve, reject) => { - const onSubmitSubscription$ = this.gs.create(SERV.PRETASKS, payload).subscribe((result) => { - const pretask = new JsonAPISerializer().deserialize({ - data: result.data - }); - preTasksIds.push(pretask.id); - resolve(); // Resolve the promise when subscription completes - }, reject); // Reject the promise if there's an error - this.unsubscribeService.add(onSubmitSubscription$); - }); - - subscriptionPromises.push(subscriptionPromise); }); - // Wait for all subscription promises to resolve - Promise.all(subscriptionPromises) - .then(() => { - resolve(preTasksIds); - }) - .catch(reject); - }); - } - - /** - * Handles the submission of the form to create a new super task. - * If the form is valid, it asynchronously performs the following steps: - * 1. Calls preTasks to create preTasks based on the form data. - * 2. Calls superTask with the created preTasks IDs and the super task name. - * Any errors that occur during the process are caught and logged. - * @returns {Promise} A promise that resolves when the submission process is completed. - */ - async onSubmit(): Promise { - if (this.createForm.valid) { - try { - this.isLoading = true; // Show spinner - const ids = await this.preTasks(this.createForm.value); - this.superTask(this.createForm.value.name, ids); - } catch (error) { - console.error('Error in preTasks:', error); - // Handle error if needed - } finally { - this.isLoading = false; // Hide spinner regardless of success or error - } + this.unsubscribeService.add(subscription$); } else { this.createForm.markAllAsTouched(); this.createForm.updateValueAndValidity(); } } - - /** - * Creates a new super task with the given name and preTasks IDs. - * @param {string} name - The name of the super task. - * @param {string[]} ids - An array of preTasks IDs to be associated with the super task. - * @returns {void} - */ - private superTask(name: string, ids: number[]) { - const payload = { supertaskName: name, pretasks: ids }; - const createSubscription$ = this.gs.create(SERV.SUPER_TASKS, payload).subscribe(() => { - this.alert.showSuccessMessage('New Supertask Mask created'); - this.router.navigate(['/tasks/supertasks']); - }); - - this.unsubscribeService.add(createSubscription$); - this.isLoading = false; - } } diff --git a/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.spec.ts b/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.spec.ts new file mode 100644 index 00000000..946b5859 --- /dev/null +++ b/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.spec.ts @@ -0,0 +1,348 @@ +import { of, throwError } from 'rxjs'; + +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { UiSettings } from '@models/config-ui.schema'; + +import { SERV } from '@services/main.config'; +import { GlobalService } from '@services/main.service'; +import { AlertService } from '@services/shared/alert.service'; +import { AutoTitleService } from '@services/shared/autotitle.service'; +import { UIConfigService } from '@services/shared/storage.service'; +import { UnsubscribeService } from '@services/unsubscribe.service'; + +import { WrbulkComponent } from '@src/app/tasks/import-supertasks/wrbulk/wrbulk.component'; + +const MOCK_CRACKER_TYPES_RESPONSE = { + data: [{ id: '1', type: 'CrackerTypes', attributes: { typeName: 'hashcat' } }], + included: [] +}; + +describe('WrbulkComponent', () => { + let component: WrbulkComponent; + let fixture: ComponentFixture; + + let globalServiceSpy: jasmine.SpyObj; + let alertServiceSpy: jasmine.SpyObj; + let routerSpy: jasmine.SpyObj; + let uiServiceSpy: jasmine.SpyObj; + + beforeEach(async () => { + globalServiceSpy = jasmine.createSpyObj('GlobalService', ['getAll', 'chelper']); + globalServiceSpy.getAll.and.returnValue(of(MOCK_CRACKER_TYPES_RESPONSE)); + globalServiceSpy.chelper.and.returnValue(of({})); + + alertServiceSpy = jasmine.createSpyObj('AlertService', ['showSuccessMessage', 'showErrorMessage']); + routerSpy = jasmine.createSpyObj('Router', ['navigate']); + routerSpy.navigate.and.returnValue(Promise.resolve(true)); + + uiServiceSpy = jasmine.createSpyObj('UIConfigService', ['getUISettings']); + uiServiceSpy.getUISettings.and.returnValue({ + hashlistAlias: '#HL#', + chunktime: 600, + statustimer: 5 + } as unknown as UiSettings); + + await TestBed.configureTestingModule({ + declarations: [WrbulkComponent], + providers: [ + { provide: AutoTitleService, useValue: jasmine.createSpyObj('AutoTitleService', ['set']) }, + { provide: UIConfigService, useValue: uiServiceSpy }, + { provide: GlobalService, useValue: globalServiceSpy }, + { provide: AlertService, useValue: alertServiceSpy }, + { provide: Router, useValue: routerSpy }, + { provide: UnsubscribeService, useValue: jasmine.createSpyObj('UnsubscribeService', ['add', 'unsubscribeAll']) } + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + .overrideComponent(WrbulkComponent, { set: { template: '' } }) + .compileComponents(); + + fixture = TestBed.createComponent(WrbulkComponent); + component = fixture.componentInstance; + }); + + // ────────────────────────────────────────────── + // Component creation + // ────────────────────────────────────────────── + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + // ────────────────────────────────────────────── + // Form initialization + // ────────────────────────────────────────────── + + it('should have an invalid form by default (name required)', () => { + // attackCmd starts with '#HL#' from uiSettings, but name is empty + expect(component.createForm.valid).toBe(false); + }); + + it('should initialize attackCmd with hashlistAlias from UIConfigService', () => { + expect(component.createForm.value.attackCmd).toBe('#HL#'); + }); + + it('should have default form values', () => { + const val = component.createForm.value; + expect(val.maxAgents).toBe(0); + expect(val.isSmall).toBe(false); + expect(val.isCpuTask).toBe(false); + expect(val.useNewBench).toBe(true); + expect(val.crackerBinaryId).toBe(1); + expect(val.baseFiles).toEqual([]); + expect(val.iterFiles).toEqual([]); + }); + + // ────────────────────────────────────────────── + // onSubmit — invalid form + // ────────────────────────────────────────────── + + it('should not call chelper when form is invalid', async () => { + await component.onSubmit(); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should mark form as touched when submitting invalid form', async () => { + await component.onSubmit(); + expect(component.createForm.touched).toBe(true); + }); + + // ────────────────────────────────────────────── + // onSubmit — client-side validations + // ────────────────────────────────────────────── + + it('should show error if crackerBinaryId is falsy (0)', async () => { + component.createForm.patchValue({ + name: 'Test', + attackCmd: '#HL# -a 0 FILE dict.txt', + crackerBinaryId: 0, + iterFiles: [1] + }); + await component.onSubmit(); + + expect(alertServiceSpy.showErrorMessage).toHaveBeenCalledWith('Invalid cracker type ID!'); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should show error if attackCmd does not contain hashlist alias', async () => { + component.createForm.patchValue({ + name: 'Test', + attackCmd: '-a 0 FILE dict.txt', + iterFiles: [1] + }); + await component.onSubmit(); + + expect(alertServiceSpy.showErrorMessage).toHaveBeenCalledWith('Command line must contain hashlist alias (#HL#)!'); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should show error if attackCmd does not contain FILE placeholder', async () => { + component.createForm.patchValue({ + name: 'Test', + attackCmd: '#HL# -a 0 dict.txt', + iterFiles: [1] + }); + await component.onSubmit(); + + expect(alertServiceSpy.showErrorMessage).toHaveBeenCalledWith('No placeholder (FILE) for the iteration!'); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should show error if no iter files are selected', async () => { + component.createForm.patchValue({ + name: 'Test', + attackCmd: '#HL# -a 0 FILE', + iterFiles: [] + }); + await component.onSubmit(); + + expect(alertServiceSpy.showErrorMessage).toHaveBeenCalledWith('You need to select at least one iteration file!'); + expect(globalServiceSpy.chelper).not.toHaveBeenCalled(); + }); + + it('should accumulate multiple validation errors without stopping at the first', async () => { + component.createForm.patchValue({ + name: 'Test', + attackCmd: 'no-alias-no-file', + crackerBinaryId: 0, + iterFiles: [] + }); + await component.onSubmit(); + + // All four checks should fire + expect(alertServiceSpy.showErrorMessage).toHaveBeenCalledTimes(4); + }); + + // ────────────────────────────────────────────── + // onSubmit — valid submission calls chelper + // ────────────────────────────────────────────── + + it('should call chelper with bulkSupertaskBuilder on valid form', async () => { + component.createForm.patchValue({ + name: 'Bulk Test', + attackCmd: '#HL# -a 0 FILE', + baseFiles: [1, 2], + iterFiles: [3, 4] + }); + await component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'bulkSupertaskBuilder', + jasmine.objectContaining({ + name: 'Bulk Test', + command: '#HL# -a 0 FILE', + basefiles: [1, 2], + iterfiles: [3, 4] + }) + ); + }); + + it('should map form fields to correct payload keys', async () => { + component.createForm.patchValue({ + name: 'Mapped', + attackCmd: '#HL# -a 0 FILE', + isCpuTask: true, + isSmall: true, + useNewBench: true, + crackerBinaryId: 5, + maxAgents: 3, + baseFiles: [10], + iterFiles: [20] + }); + await component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith(SERV.HELPER, 'bulkSupertaskBuilder', { + name: 'Mapped', + command: '#HL# -a 0 FILE', + isCpu: true, + isSmall: true, + crackerBinaryTypeId: 5, + benchtype: 'speed', + maxAgents: 3, + basefiles: [10], + iterfiles: [20] + }); + }); + + it('should set benchtype to "runtime" when useNewBench is false', async () => { + component.createForm.patchValue({ + name: 'Runtime', + attackCmd: '#HL# FILE', + useNewBench: false, + iterFiles: [1] + }); + await component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'bulkSupertaskBuilder', + jasmine.objectContaining({ benchtype: 'runtime' }) + ); + }); + + it('should set benchtype to "speed" when useNewBench is true', async () => { + component.createForm.patchValue({ + name: 'Speed', + attackCmd: '#HL# FILE', + useNewBench: true, + iterFiles: [1] + }); + await component.onSubmit(); + + expect(globalServiceSpy.chelper).toHaveBeenCalledWith( + SERV.HELPER, + 'bulkSupertaskBuilder', + jasmine.objectContaining({ benchtype: 'speed' }) + ); + }); + + // ────────────────────────────────────────────── + // onSubmit — success flow + // ────────────────────────────────────────────── + + it('should show success message and navigate on success', async () => { + component.createForm.patchValue({ + name: 'OK', + attackCmd: '#HL# FILE', + iterFiles: [1] + }); + await component.onSubmit(); + + expect(alertServiceSpy.showSuccessMessage).toHaveBeenCalledWith('New Supertask Wordlist/Rules Bulk created'); + expect(routerSpy.navigate).toHaveBeenCalledWith(['/tasks/supertasks']); + }); + + // ────────────────────────────────────────────── + // onSubmit — error flow + // ────────────────────────────────────────────── + + it('should reset isLoading on chelper error', async () => { + globalServiceSpy.chelper.and.returnValue(throwError(() => new Error('Server error'))); + component.createForm.patchValue({ + name: 'Err', + attackCmd: '#HL# FILE', + iterFiles: [1] + }); + await component.onSubmit(); + + expect(component.isLoading).toBe(false); + }); + + it('should not navigate on chelper error', async () => { + globalServiceSpy.chelper.and.returnValue(throwError(() => new Error('Server error'))); + component.createForm.patchValue({ + name: 'Err', + attackCmd: '#HL# FILE', + iterFiles: [1] + }); + await component.onSubmit(); + + expect(routerSpy.navigate).not.toHaveBeenCalled(); + }); + + // ────────────────────────────────────────────── + // getFormData + // ────────────────────────────────────────────── + + it('should return current form data from getFormData()', () => { + component.createForm.patchValue({ + attackCmd: '#HL# -a 0 FILE', + baseFiles: [1], + iterFiles: [2] + }); + + expect(component.getFormData()).toEqual({ + attackCmd: '#HL# -a 0 FILE', + files: [1], + otherFiles: [2] + }); + }); + + // ────────────────────────────────────────────── + // onUpdateForm + // ────────────────────────────────────────────── + + it('should update attackCmd and baseFiles on CMD event', () => { + component.onUpdateForm({ + type: 'CMD', + attackCmd: '#HL# -a 0 dict.txt FILE', + files: [10, 20] + }); + + expect(component.createForm.value.attackCmd).toBe('#HL# -a 0 dict.txt FILE'); + expect(component.createForm.value.baseFiles).toEqual([10, 20]); + }); + + it('should update iterFiles on non-CMD event', () => { + component.onUpdateForm({ + type: 'ITER', + otherFiles: [30, 40] + }); + + expect(component.createForm.value.iterFiles).toEqual([30, 40]); + }); +}); diff --git a/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.ts b/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.ts index d0e73ff6..0a917a7a 100644 --- a/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.ts +++ b/src/app/tasks/import-supertasks/wrbulk/wrbulk.component.ts @@ -1,13 +1,9 @@ -import { firstValueFrom } from 'rxjs'; - import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { JCrackerBinaryType } from '@models/cracker-binary.model'; -import { JFile } from '@models/file.model'; import { HorizontalNav } from '@models/horizontalnav.model'; -import { JPretask } from '@models/pretask.model'; import { ResponseWrapper } from '@models/response.model'; import { JsonAPISerializer } from '@services/api/serializer-service'; @@ -119,63 +115,38 @@ export class WrbulkComponent implements OnInit, OnDestroy { } /** - * Create pre-tasks asynchronously. + * Create pre-tasks and supertask via the backend helper. * * @param {Object} form - The form data containing task configurations. - * @returns {Promise} A Promise that resolves with an array of pre-task IDs. */ - private async preTasks(form): Promise { - const preTasksIds: number[] = []; - const iterFiles: number[] = form.iterFiles; - - try { - const promises = iterFiles.map(async (iter, index) => { - const payload = { - taskName: '', - attackCmd: '', - maxAgents: form.maxAgents, - chunkTime: this.uiService.getUISettings()?.chunktime ?? 0, - statusTimer: this.uiService.getUISettings()?.statustimer ?? 0, - priority: index + 1, - color: '', - isCpuTask: form.isCpuTask, - crackerBinaryTypeId: form.crackerBinaryId, - isSmall: form.isSmall, - useNewBench: form.useNewBench, - isMaskImport: true, - files: form.baseFiles - }; - - // Get file name - const fileName: string = await new Promise((resolve, reject) => { - const fileSubscription$ = this.gs.get(SERV.FILES, iter).subscribe({ - next: (response: ResponseWrapper) => { - const file = new JsonAPISerializer().deserialize({ - data: response.data, - included: response.included - }); - resolve(file.filename); - }, - error: reject - }); - - this.unsubscribeService.add(fileSubscription$); - }); - - const updatedAttackCmd = form.attackCmd.replace('FILE', fileName); - payload.taskName = form.name + ' + ' + fileName; - payload.attackCmd = updatedAttackCmd; + private createSupertask(form): void { + const payload = { + name: form.name, + command: form.attackCmd, + isCpu: form.isCpuTask, + isSmall: form.isSmall, + crackerBinaryTypeId: form.crackerBinaryId, + benchtype: form.useNewBench ? 'speed' : 'runtime', + maxAgents: form.maxAgents, + basefiles: form.baseFiles, + iterfiles: form.iterFiles + }; - const result: ResponseWrapper = await firstValueFrom(this.gs.create(SERV.PRETASKS, payload)); - const pretask = new JsonAPISerializer().deserialize({ data: result.data, included: result.included }); - preTasksIds.push(pretask.id); - }); + const subscription$ = this.gs.chelper(SERV.HELPER, 'bulkSupertaskBuilder', payload).subscribe({ + next: () => { + this.alert.showSuccessMessage('New Supertask Wordlist/Rules Bulk created'); + this.router.navigate(['/tasks/supertasks']); + }, + error: (error) => { + console.error('Error creating bulk supertask:', error); + this.isLoading = false; + }, + complete: () => { + this.isLoading = false; + } + }); - await Promise.all(promises); - return preTasksIds; - } catch (error) { - return Promise.reject(error); - } + this.unsubscribeService.add(subscription$); } /** @@ -232,13 +203,11 @@ export class WrbulkComponent implements OnInit, OnDestroy { } this.isLoading = true; // Show spinner - const ids = await this.preTasks(formValue); - this.superTask(formValue.name, ids); + this.createSupertask(formValue); } catch (error) { console.error('Error when importing supertask:', error); - // Handle error if needed } finally { - this.isLoading = false; // Hide spinner regardless of success or error + this.isLoading = false; } } else { this.createForm.markAllAsTouched(); @@ -246,23 +215,6 @@ export class WrbulkComponent implements OnInit, OnDestroy { } } - /** - * Creates a new super task with the given name and preTasks IDs. - * @param {string} name - The name of the super task. - * @param {string[]} ids - An array of preTasks IDs to be associated with the super task. - * @returns {void} - */ - private superTask(name: string, ids: number[]) { - const payload = { supertaskName: name, pretasks: ids }; - const createSubscription$ = this.gs.create(SERV.SUPER_TASKS, payload).subscribe(() => { - this.alert.showSuccessMessage('New Supertask Wordlist/Rules Bulk created'); - this.router.navigate(['/tasks/supertasks']); - }); - - this.unsubscribeService.add(createSubscription$); - this.isLoading = false; - } - /** * Retrieves the form data containing attack command and files. * @returns An object with attack command and files.