diff --git a/app/adapters/available-automated-device.ts b/app/adapters/available-automated-device.ts new file mode 100644 index 000000000..73bbc3bda --- /dev/null +++ b/app/adapters/available-automated-device.ts @@ -0,0 +1,13 @@ +import CommondrfNestedAdapter from './commondrf-nested'; + +export default class AvailableAutomatedDeviceAdapter extends CommondrfNestedAdapter { + setNestedUrlNamespace(projectId: string) { + this.namespace = `${this.namespace_v2}/projects/${projectId}`; + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'available-automated-device': AvailableAutomatedDeviceAdapter; + } +} diff --git a/app/adapters/available-manual-device.ts b/app/adapters/available-manual-device.ts new file mode 100644 index 000000000..865933a32 --- /dev/null +++ b/app/adapters/available-manual-device.ts @@ -0,0 +1,13 @@ +import CommondrfNestedAdapter from './commondrf-nested'; + +export default class AvailableManualDeviceAdapter extends CommondrfNestedAdapter { + setNestedUrlNamespace(projectId: string) { + this.namespace = `${this.namespace_v2}/projects/${projectId}`; + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'available-manual-device': AvailableManualDeviceAdapter; + } +} diff --git a/app/adapters/commondrf-nested.ts b/app/adapters/commondrf-nested.ts new file mode 100644 index 000000000..9cbd32301 --- /dev/null +++ b/app/adapters/commondrf-nested.ts @@ -0,0 +1,30 @@ +import CommonDRFAdapter from './commondrf'; +import { underscore } from '@ember/string'; +import type ModelRegistry from 'ember-data/types/registries/model'; + +export default class CommondrfNestedAdapter extends CommonDRFAdapter { + namespace = ''; + pathTypeName: keyof ModelRegistry | null = null; + + pathForType(type: keyof ModelRegistry) { + return underscore(super.pathForType(this.pathTypeName || type)); + } + + handleResponse( + status: number, + headers: object, + payload: object, + requestData: object + ) { + if (status >= 400 && this.namespace === '') { + throw new Error( + 'setNestUrlNamespace should be called before making a request' + ); + } + + // reset namespace + this.namespace = ''; + + return super.handleResponse(status, headers, payload, requestData); + } +} diff --git a/app/adapters/ds-automated-device-preference.ts b/app/adapters/ds-automated-device-preference.ts new file mode 100644 index 000000000..7f1731512 --- /dev/null +++ b/app/adapters/ds-automated-device-preference.ts @@ -0,0 +1,19 @@ +import CommondrfNestedAdapter from './commondrf-nested'; + +export default class DsAutomatedDevicePreferenceAdapter extends CommondrfNestedAdapter { + _buildURL() { + return this.buildURLFromBase( + `${this.namespace}/ds_automated_device_preference` + ); + } + + setNestedUrlNamespace(profileId: string) { + this.namespace = `${this.namespace_v2}/profiles/${profileId}`; + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'ds-automated-device-preference': DsAutomatedDevicePreferenceAdapter; + } +} diff --git a/app/adapters/ds-automation-preference.ts b/app/adapters/ds-automation-preference.ts new file mode 100644 index 000000000..ca248c12b --- /dev/null +++ b/app/adapters/ds-automation-preference.ts @@ -0,0 +1,17 @@ +import CommondrfNestedAdapter from './commondrf-nested'; + +export default class DsAutomationPreferenceAdapter extends CommondrfNestedAdapter { + _buildURL() { + return this.buildURLFromBase(`${this.namespace}/automation_preference`); + } + + setNestedUrlNamespace(profileId: string) { + this.namespace = `${this.namespace_v2}/profiles/${profileId}`; + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'ds-automation-preference': DsAutomationPreferenceAdapter; + } +} diff --git a/app/adapters/ds-manual-device-preference.ts b/app/adapters/ds-manual-device-preference.ts new file mode 100644 index 000000000..466e61f75 --- /dev/null +++ b/app/adapters/ds-manual-device-preference.ts @@ -0,0 +1,19 @@ +import CommondrfNestedAdapter from './commondrf-nested'; + +export default class DsManualDevicePreferenceAdapter extends CommondrfNestedAdapter { + _buildURL() { + return this.buildURLFromBase( + `${this.namespace}/ds_manual_device_preference` + ); + } + + setNestedUrlNamespace(profileId: string | number) { + this.namespace = `${this.namespace_v2}/profiles/${profileId}`; + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'ds-manual-device-preference': DsManualDevicePreferenceAdapter; + } +} diff --git a/app/adapters/dynamicscan.ts b/app/adapters/dynamicscan.ts index a08fc22e8..2d301de42 100644 --- a/app/adapters/dynamicscan.ts +++ b/app/adapters/dynamicscan.ts @@ -6,7 +6,7 @@ export default class DynamicscanAdapter extends commondrf { extendTime(modelName: string, snapshot: DynamicscanModel, time: number) { const id = snapshot.id; - const url = this.buildURL(modelName, id); + const url = `${this.buildURL(modelName, id)}/extend`; return this.ajax(url, 'PUT', { data: { time }, diff --git a/app/adapters/file.ts b/app/adapters/file.ts index ab3923c62..89d9da986 100644 --- a/app/adapters/file.ts +++ b/app/adapters/file.ts @@ -1,25 +1,45 @@ -import commondrf from './commondrf'; +import CommonDRFAdapter from './commondrf'; +import type DynamicscanModel from 'irene/models/dynamicscan'; interface ProjectFilesQuery { projectId: string; } -export default class File extends commondrf { +export default class File extends CommonDRFAdapter { _buildURL(_: string | number, id: string | number) { if (id) { const baseurl = `${this.namespace_v2}/files`; + return this.buildURLFromBase(`${baseurl}/${encodeURIComponent(id)}`); } } _buildNestedURL(modelName: string | number, projectId: string) { const filesURL = `${this.namespace}/projects/${projectId}/files`; + return this.buildURLFromBase(filesURL); } urlForQuery(query: ProjectFilesQuery, modelName: string | number) { return this._buildNestedURL(modelName, query.projectId); } + + async getLastDynamicScan( + fileId: string, + mode: number + ): Promise { + const url = `${this._buildURL('file', fileId)}/dynamicscans?limit=1&mode=${mode}`; + + const res = await this.ajax(url, 'GET'); + + if (res.results[0]) { + const normailized = this.store.normalize('dynamicscan', res.results[0]); + + return this.store.push(normailized) as DynamicscanModel; + } + + return null; + } } declare module 'ember-data/types/registries/adapter' { diff --git a/app/adapters/profile.ts b/app/adapters/profile.ts index 2c1422ed1..ee051b639 100644 --- a/app/adapters/profile.ts +++ b/app/adapters/profile.ts @@ -4,8 +4,6 @@ import ProfileModel, { type ProfileRegulatoryReportPreference, type SaveReportPreferenceData, type SetProfileRegulatorPrefData, - type SetProfileDSAutomatedDevicePrefData, - type SetProfileDSManualDevicePrefData, } from 'irene/models/profile'; export default class ProfileAdapter extends commondrf { @@ -19,50 +17,6 @@ export default class ProfileAdapter extends commondrf { return this.buildURLFromBase(baseurl); } - buildDSManualDevicePrefUrl(modelId: string | number) { - return ( - this._buildURL('profile', modelId, this.namespace_v2) + - '/ds_manual_device_preference' - ); - } - - buildDSAutomatedDevicePrefUrl(modelId: string | number) { - return ( - this._buildURL('profile', modelId, this.namespace_v2) + - '/ds_automated_device_preference' - ); - } - - async setDSManualDevicePrefData( - modelInstance: ProfileModel, - data: SetProfileDSManualDevicePrefData - ) { - const url = this.buildDSManualDevicePrefUrl(modelInstance.get('id')); - - return this.ajax(url, 'PUT', { data }); - } - - async setDSAutomatedDevicePrefData( - modelInstance: ProfileModel, - data: SetProfileDSAutomatedDevicePrefData - ) { - const url = this.buildDSAutomatedDevicePrefUrl(modelInstance.get('id')); - - return this.ajax(url, 'PUT', { data }); - } - - async getDsManualDevicePreference(modelInstance: ProfileModel) { - const url = this.buildDSManualDevicePrefUrl(modelInstance.get('id')); - - return this.ajax(url, 'GET'); - } - - async getDsAutomatedDevicePreference(modelInstance: ProfileModel) { - const url = this.buildDSAutomatedDevicePrefUrl(modelInstance.get('id')); - - return this.ajax(url, 'GET'); - } - async saveReportPreference( modelInstance: ProfileModel, data: SaveReportPreferenceData diff --git a/app/components/ds-preference-provider/index.hbs b/app/components/ds-preference-provider/index.hbs new file mode 100644 index 000000000..503984d81 --- /dev/null +++ b/app/components/ds-preference-provider/index.hbs @@ -0,0 +1,15 @@ +{{yield + (hash + dsManualDevicePreference=this.dsManualDevicePreference + loadingDsManualDevicePref=this.fetchDsManualDevicePref.isRunning + dsAutomatedDevicePreference=this.dsAutomatedDevicePreference + loadingDsAutomatedDevicePref=this.fetchDsAutomatedDevicePref.isRunning + fetchAvailableDevices=(perform this.fetchAvailableDevices) + availableManualDevices=this.availableDevicesResponse + loadingAvailableDevices=this.fetchAvailableDevices.isRunning + updateDsManualDevicePref=this.handleUpdateDsManualDevicePreference + updatingDsManualDevicePref=this.updateDsManualDevicePreference.isRunning + updateDsAutomatedDevicePref=this.handleUpdateDsAutomatedDevicePreference + updatingDsAutomatedDevicePref=this.updateDsAutomatedDevicePreference.isRunning + ) +}} \ No newline at end of file diff --git a/app/components/ds-preference-provider/index.ts b/app/components/ds-preference-provider/index.ts new file mode 100644 index 000000000..7bff0910c --- /dev/null +++ b/app/components/ds-preference-provider/index.ts @@ -0,0 +1,195 @@ +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { task } from 'ember-concurrency'; +import type Store from '@ember-data/store'; +import type IntlService from 'ember-intl/services/intl'; + +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import type DS from 'ember-data'; + +import type FileModel from 'irene/models/file'; +import type DsManualDevicePreferenceModel from 'irene/models/ds-manual-device-preference'; +import type AvailableManualDeviceModel from 'irene/models/available-manual-device'; +import type DsAutomatedDevicePreferenceModel from 'irene/models/ds-automated-device-preference'; + +export interface AvailableManualDeviceQueryParams { + limit?: number; + offset?: number; + has_sim?: boolean; + has_vpn?: boolean; + has_pin_lock?: boolean; + is_reserved?: boolean; + platform_version_min?: string; + platform_version_max?: string; +} + +type AvailableManualDeviceQueryResponse = + DS.AdapterPopulatedRecordArray & { + meta: { count: number }; + }; + +export interface DsPreferenceContext { + dsManualDevicePreference: DsManualDevicePreferenceModel | null; + dsAutomatedDevicePreference: DsAutomatedDevicePreferenceModel | null; + availableManualDevices: AvailableManualDeviceQueryResponse | null; + loadingAvailableDevices: boolean; + loadingDsManualDevicePref: boolean; + loadingDsAutomatedDevicePref: boolean; + updatingDsManualDevicePref: boolean; + updatingDsAutomatedDevicePref: boolean; + + updateDsManualDevicePref: ( + devicePreference: DsManualDevicePreferenceModel + ) => void; + + updateDsAutomatedDevicePref: ( + devicePreference: DsAutomatedDevicePreferenceModel + ) => void; + + fetchAvailableDevices: ( + queryParams?: AvailableManualDeviceQueryParams + ) => void; +} + +interface DsPreferenceProviderSignature { + Args: { + file: FileModel; + profileId: string; + }; + Blocks: { + default: [DsPreferenceContext]; + }; +} + +export default class DsPreferenceProviderComponent extends Component { + @service declare intl: IntlService; + @service declare store: Store; + @service('notifications') declare notify: NotificationService; + + @tracked dsManualDevicePreference: DsManualDevicePreferenceModel | null = + null; + + @tracked + dsAutomatedDevicePreference: DsAutomatedDevicePreferenceModel | null = null; + + @tracked availableDevicesResponse: AvailableManualDeviceQueryResponse | null = + null; + + constructor(owner: unknown, args: DsPreferenceProviderSignature['Args']) { + super(owner, args); + + this.fetchDsManualDevicePref.perform(); + this.fetchDsAutomatedDevicePref.perform(); + } + + get selectedAvailableManualDevice() { + const id = this.dsManualDevicePreference?.dsManualDeviceIdentifier; + + return this.availableDevicesResponse + ?.slice() + .find((d) => d.deviceIdentifier === id); + } + + @action + handleUpdateDsManualDevicePreference( + devicePreference: DsManualDevicePreferenceModel + ) { + this.updateDsManualDevicePreference.perform(devicePreference); + } + + @action + handleUpdateDsAutomatedDevicePreference( + devicePreference: DsAutomatedDevicePreferenceModel + ) { + this.updateDsAutomatedDevicePreference.perform(devicePreference); + } + + fetchDsManualDevicePref = task(async () => { + try { + const adapter = this.store.adapterFor('ds-manual-device-preference'); + adapter.setNestedUrlNamespace(this.args.profileId); + + this.dsManualDevicePreference = await this.store.queryRecord( + 'ds-manual-device-preference', + {} + ); + } catch (error) { + this.notify.error(this.intl.t('errorFetchingDsManualDevicePref')); + } + }); + + fetchDsAutomatedDevicePref = task(async () => { + try { + const adapter = this.store.adapterFor('ds-automated-device-preference'); + adapter.setNestedUrlNamespace(this.args.profileId); + + this.dsAutomatedDevicePreference = await this.store.queryRecord( + 'ds-automated-device-preference', + {} + ); + } catch (error) { + this.notify.error(this.intl.t('errorFetchingDsAutomatedDevicePref')); + } + }); + + updateDsManualDevicePreference = task( + async (devicePreference: DsManualDevicePreferenceModel) => { + try { + const adapter = this.store.adapterFor('ds-manual-device-preference'); + adapter.setNestedUrlNamespace(this.args.profileId); + + this.dsManualDevicePreference = await devicePreference.save(); + + this.notify.success(this.intl.t('savedPreferences')); + } catch (error) { + devicePreference.rollbackAttributes(); + + this.notify.error(this.intl.t('failedToUpdateDsManualDevicePref')); + } + } + ); + + updateDsAutomatedDevicePreference = task( + async (devicePreference: DsAutomatedDevicePreferenceModel) => { + try { + const adapter = this.store.adapterFor('ds-automated-device-preference'); + adapter.setNestedUrlNamespace(this.args.profileId); + + this.dsAutomatedDevicePreference = await devicePreference.save(); + + this.notify.success(this.intl.t('savedPreferences')); + } catch (error) { + devicePreference.rollbackAttributes(); + + this.notify.error(this.intl.t('failedToUpdateDsAutomatedDevicePref')); + } + } + ); + + fetchAvailableDevices = task( + async (queryParams: AvailableManualDeviceQueryParams = {}) => { + try { + const adapter = this.store.adapterFor('available-manual-device'); + + adapter.setNestedUrlNamespace( + this.args.file.project?.get('id') as string + ); + + this.availableDevicesResponse = (await this.store.query( + 'available-manual-device', + { ...queryParams, platform_version_min: this.args.file.minOsVersion } + )) as AvailableManualDeviceQueryResponse; + } catch (error) { + this.notify.error(this.intl.t('errorFetchingAvailableDevices')); + } + } + ); +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + DsPreferenceProvider: typeof DsPreferenceProviderComponent; + } +} diff --git a/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.ts b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.ts index 3664ba959..18c61fce7 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.ts +++ b/app/components/file-details/dynamic-scan/action/drawer/automated-dast/index.ts @@ -14,7 +14,7 @@ import { dsAutomatedDevicePref } from 'irene/helpers/ds-automated-device-pref'; import type IntlService from 'ember-intl/services/intl'; import type Store from '@ember-data/store'; import type ApiScanOptionsModel from 'irene/models/api-scan-options'; -import type { DevicePreferenceContext } from 'irene/components/project-preferences/provider'; +import type { DsPreferenceContext } from 'irene/components/ds-preference-provider'; import type ScanParameterGroupModel from 'irene/models/scan-parameter-group'; import type FileModel from 'irene/models/file'; import type ProxySettingModel from 'irene/models/proxy-setting'; @@ -26,8 +26,7 @@ export interface FileDetailsDynamicScanDrawerAutomatedDastSignature { Element: HTMLElement; Args: { file: FileModel; - dpContext: DevicePreferenceContext; - enableApiScan(event: Event, checked: boolean): void; + dpContext: DsPreferenceContext; }; } @@ -95,7 +94,7 @@ export default class FileDetailsDynamicScanDrawerAutomatedDastComponent extends get minOSVersion() { const version = - this.automatedDastDevicePreferences?.ds_automated_platform_version_min; + this.automatedDastDevicePreferences?.dsAutomatedPlatformVersionMin; return isEmpty(version) ? '-' : version; } @@ -108,8 +107,7 @@ export default class FileDetailsDynamicScanDrawerAutomatedDastComponent extends value: this.intl.t( dsAutomatedDevicePref([ Number( - this.automatedDastDevicePreferences - ?.ds_automated_device_selection || + this.automatedDastDevicePreferences?.dsAutomatedDeviceSelection || ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA ), ]) diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.ts b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.ts index 1fceaf2d7..77a0ad6cc 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.ts +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/device-capabilities/index.ts @@ -1,16 +1,16 @@ import Component from '@glimmer/component'; -import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; +import type AvailableManualDeviceModel from 'irene/models/available-manual-device'; enum CapabilitiesTranslationsMap { hasSim = 'sim', hasVpn = 'vpn', hasPinLock = 'pinLock', - hasVnc = 'vnc', + // hasVnc = 'vnc', } export interface FileDetailsDynamicScanDrawerDevicePrefTableDeviceCapabilitiesSignature { Args: { - deviceProps: ProjectAvailableDeviceModel; + deviceProps: AvailableManualDeviceModel; }; } diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs index 996d76bbb..6fb93325a 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/index.hbs @@ -13,30 +13,28 @@ - {{this.getSelectedFilterOptionLabel dPrefFilter}} + {{opt.label}} - {{#unless this.showEmptyAvailableDeviceList}} - {{#if @isFetchingManualDevices}} - - - - - - - + {{#if @dpContext.loadingAvailableDevices}} + + + + + + + - {{else}} - - + + - - {{#if r.columnValue.component}} - {{#let (component r.columnValue.component) as |Component|}} - - {{/let}} - {{else}} - - {{value}} - - {{/if}} - - - - {{/if}} - {{/unless}} + {{#if r.columnValue.component}} + {{#let (component r.columnValue.component) as |Component|}} + + {{/let}} + {{else}} + + {{value}} + + {{/if}} + + + + {{/if}} - {{#if this.showEmptyAvailableDeviceList}} + {{#if this.showEmptyDeviceListContent}} {{/if}} - {{#unless this.showEmptyAvailableDeviceList}} + {{#unless this.hasNoAvailableManualDevice}} { @service declare store: Store; @service('notifications') declare notify: NotificationService; - @service declare ajax: any; @service declare intl: IntlService; @tracked limit = 5; @tracked offset = 0; - @tracked filteredManualDevices: ProjectAvailableDeviceModel[] = []; - @tracked selectedDevicePrefFilterKey: DevicePrefFilterKey = - 'ALL_AVAILABLE_DEVICES'; + @tracked selectedDeviceFilter = this + .deviceFilterOptions[0] as AvailableManualDeviceFilterOption; + + constructor( + owner: unknown, + args: FileDetailsDynamicScanDrawerDevicePrefTableSignature['Args'] + ) { + super(owner, args); + + this.handleFetchAvailableDevices(); + } + + get dpContext() { + return this.args.dpContext; + } - get allAvailableManualDevices() { - return this.args.allAvailableManualDevices; + get devicePreference() { + return this.dpContext.dsManualDevicePreference; } get loadingMockData() { @@ -87,39 +99,24 @@ export default class FileDetailsDynamicScanDrawerDevicePrefTableComponent extend ]; } - get showAllManualDevices() { - return this.selectedDevicePrefFilterKey === 'ALL_AVAILABLE_DEVICES'; - } - - get currentDevicesInView() { - let data = this.showAllManualDevices - ? [...this.allAvailableManualDevices] - : [...this.filteredManualDevices]; - - if (data.length >= this.limit) { - data = data.splice(this.offset, this.limit); - } - - return data; - } - - get selectedFilterKeyLabelMap() { - return { - ALL_AVAILABLE_DEVICES: this.intl.t( - 'modalCard.dynamicScan.allAvailableDevices' - ), - DEVICES_WITH_SIM: this.intl.t('modalCard.dynamicScan.devicesWithSim'), - DEVICES_WITH_VPN: this.intl.t('modalCard.dynamicScan.devicesWithVPN'), - DEVICES_WITH_LOCK: this.intl.t('modalCard.dynamicScan.devicesWithLock'), - }; - } - - get devicePreferenceTypes() { + get deviceFilterOptions() { return [ - 'ALL_AVAILABLE_DEVICES' as const, - 'DEVICES_WITH_SIM' as const, - 'DEVICES_WITH_VPN' as const, - 'DEVICES_WITH_LOCK' as const, + { + label: this.intl.t('modalCard.dynamicScan.allAvailableDevices'), + value: AvailableManualDeviceFilterKey.ALL_AVAILABLE_DEVICES, + }, + { + label: this.intl.t('modalCard.dynamicScan.devicesWithSim'), + value: AvailableManualDeviceFilterKey.DEVICES_WITH_SIM, + }, + { + label: this.intl.t('modalCard.dynamicScan.devicesWithVPN'), + value: AvailableManualDeviceFilterKey.DEVICES_WITH_VPN, + }, + { + label: this.intl.t('modalCard.dynamicScan.devicesWithLock'), + value: AvailableManualDeviceFilterKey.DEVICES_WITH_LOCK, + }, ]; } @@ -127,57 +124,74 @@ export default class FileDetailsDynamicScanDrawerDevicePrefTableComponent extend return styles['filter-input']; } - get showEmptyAvailableDeviceList() { - return !this.showAllManualDevices && this.filteredManualDevices.length < 1; + get availableManualDevices() { + return this.dpContext.availableManualDevices?.slice() || []; } - get totalItemsCount() { - return this.showAllManualDevices - ? this.allAvailableManualDevices.length - : this.filteredManualDevices.length; + get hasNoAvailableManualDevice() { + return this.totalAvailableManualDevicesCount === 0; } - @action getSelectedFilterOptionLabel(opt: DevicePrefFilterKey) { - return this.selectedFilterKeyLabelMap[opt]; + get totalAvailableManualDevicesCount() { + return this.dpContext.availableManualDevices?.meta?.count || 0; } - @action setDevicePrefFilter(opt: DevicePrefFilterKey) { - this.selectedDevicePrefFilterKey = opt; + get showEmptyDeviceListContent() { + return ( + !this.dpContext.loadingAvailableDevices && this.hasNoAvailableManualDevice + ); + } - this.goToPage({ limit: this.limit, offset: 0 }); + @action + handleDeviceFilterChange(opt: AvailableManualDeviceFilterOption) { + this.offset = 0; + this.selectedDeviceFilter = opt; - this.filterAvailableDevices.perform(opt); + this.handleFetchAvailableDevices(); } - @action setSelectedDevice(device: ProjectAvailableDeviceModel) { - this.args.dpContext.handleSelectDsManualIdentifier(device.deviceIdentifier); + @action + setSelectedDevice(device: AvailableManualDeviceModel) { + const preference = this.devicePreference as DsManualDevicePreferenceModel; + + preference.dsManualDeviceSelection = + ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE; + + preference.dsManualDeviceIdentifier = device.deviceIdentifier; + + this.dpContext.updateDsManualDevicePref(preference); } // Table Actions - @action goToPage(args: PaginationProviderActionsArgs) { - const { limit, offset } = args; - + @action + goToPage({ limit, offset }: PaginationProviderActionsArgs) { this.limit = limit; this.offset = offset; - } - @action onItemPerPageChange(args: PaginationProviderActionsArgs) { - const { limit } = args; - const offset = 0; + this.handleFetchAvailableDevices(); + } + @action + onItemPerPageChange({ limit }: PaginationProviderActionsArgs) { this.limit = limit; - this.offset = offset; + this.offset = 0; + + this.handleFetchAvailableDevices(); } - filterAvailableDevices = task(async (filterkey: DevicePrefFilterKey) => { - const modelFilterKey = AvailableManualDeviceModelKeyMap[ - filterkey - ] as keyof ProjectAvailableDeviceModel; + @action + handleFetchAvailableDevices() { + const filter = this.selectedDeviceFilter.value; - this.filteredManualDevices = this.allAvailableManualDevices.filter( - (dev) => filterkey === 'ALL_AVAILABLE_DEVICES' || dev[modelFilterKey] - ); - }); + const isAllDevices = + filter === AvailableManualDeviceFilterKey.ALL_AVAILABLE_DEVICES; + + this.dpContext.fetchAvailableDevices({ + limit: this.limit, + offset: this.offset, + ...(isAllDevices ? {} : { [filter]: true }), + }); + } } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.ts b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.ts index b08680ab1..f9ffe7873 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.ts +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/selected-device/index.ts @@ -1,12 +1,12 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; -import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; +import type AvailableManualDeviceModel from 'irene/models/available-manual-device'; export interface FileDetailsDynamicScanDrawerDevicePrefTableSelectedDeviceSignature { Args: { - deviceProps: ProjectAvailableDeviceModel; + deviceProps: AvailableManualDeviceModel; selectedDeviceId?: string; - onDeviceClick(device: ProjectAvailableDeviceModel): void; + onDeviceClick(device: AvailableManualDeviceModel): void; }; } diff --git a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.ts b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.ts index 108579efe..d8dc589e9 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.ts +++ b/app/components/file-details/dynamic-scan/action/drawer/device-pref-table/type/index.ts @@ -1,11 +1,11 @@ import Component from '@glimmer/component'; -import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; + import ENUMS from 'irene/enums'; +import type AvailableManualDeviceModel from 'irene/models/available-manual-device'; export interface FileDetailsDynamicScanDrawerDevicePrefTableTypeSignature { Args: { - deviceProps: ProjectAvailableDeviceModel; - selectedDevice: ProjectAvailableDeviceModel; + deviceProps: AvailableManualDeviceModel; }; } diff --git a/app/components/file-details/dynamic-scan/action/drawer/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/index.hbs index 2c6934ea0..052a0b424 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/index.hbs +++ b/app/components/file-details/dynamic-scan/action/drawer/index.hbs @@ -49,16 +49,13 @@ {{else}} {{/if}} diff --git a/app/components/file-details/dynamic-scan/action/drawer/index.ts b/app/components/file-details/dynamic-scan/action/drawer/index.ts index 30cf53d18..363b80e26 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/index.ts +++ b/app/components/file-details/dynamic-scan/action/drawer/index.ts @@ -9,22 +9,21 @@ import ENV from 'irene/config/environment'; import triggerAnalytics from 'irene/utils/trigger-analytics'; import ENUMS from 'irene/enums'; import parseError from 'irene/utils/parse-error'; -import { ProfileDynamicScanMode } from 'irene/models/profile'; import type IntlService from 'ember-intl/services/intl'; import type Store from '@ember-data/store'; import type FileModel from 'irene/models/file'; -import { type DevicePreferenceContext } from 'irene/components/project-preferences/provider'; -import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; +import type DynamicscanModel from 'irene/models/dynamicscan'; +import type { DsPreferenceContext } from 'irene/components/ds-preference-provider'; export interface FileDetailsDynamicScanActionDrawerSignature { Args: { + dpContext: DsPreferenceContext; onClose: () => void; - pollDynamicStatus: () => void; + onScanStart: (dynamicscan: DynamicscanModel) => void; file: FileModel; isAutomatedScan?: boolean; - dpContext: DevicePreferenceContext; }; } @@ -34,19 +33,7 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone @service declare store: Store; @service('notifications') declare notify: NotificationService; - @tracked isApiScanEnabled = false; - @tracked allAvailableManualDevices: ProjectAvailableDeviceModel[] = []; - - constructor( - owner: unknown, - args: FileDetailsDynamicScanActionDrawerSignature['Args'] - ) { - super(owner, args); - - if (!this.args.isAutomatedScan) { - this.fetchAllAvailableManualDevices.perform(); - } - } + @tracked isApiCaptureEnabled = false; get file() { return this.args.file; @@ -56,6 +43,10 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone return this.file.project.get('id'); } + get profileId() { + return this.file.profile.get('id') as string; + } + get tStartingScan() { return this.intl.t('startingScan'); } @@ -69,15 +60,17 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone } get dsManualDeviceIdentifier() { - return this.args.dpContext?.dsManualDevicePreference - ?.ds_manual_device_identifier; + return this.args.dpContext.dsManualDevicePreference + ?.dsManualDeviceIdentifier; } get selectedManualDeviceIsInAvailableDeviceList() { return ( - this.allAvailableManualDevices.findIndex( - (d) => d.deviceIdentifier === this.dsManualDeviceIdentifier - ) !== -1 + this.args.dpContext.availableManualDevices + ?.slice() + ?.findIndex( + (d) => d.deviceIdentifier === this.dsManualDeviceIdentifier + ) !== -1 ); } @@ -91,7 +84,7 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE; const dsManualDeviceSelection = - dpContext.dsManualDevicePreference?.ds_manual_device_selection; + dpContext.dsManualDevicePreference?.dsManualDeviceSelection; return ( dsManualDeviceSelection === anyDeviceSelection || @@ -105,8 +98,8 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone } @action - enableApiScan(_: Event, checked: boolean) { - this.isApiScanEnabled = !!checked; + handleApiCaptureChange(_: Event, checked: boolean) { + this.isApiCaptureEnabled = !!checked; } @action @@ -122,12 +115,12 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone startDynamicScan = task(async () => { try { const mode = this.args.isAutomatedScan - ? ProfileDynamicScanMode.AUTOMATED - : ProfileDynamicScanMode.MANUAL; + ? ENUMS.DYNAMIC_MODE.AUTOMATED + : ENUMS.DYNAMIC_MODE.MANUAL; const data = { mode, - enable_api_capture: this.isApiScanEnabled, + enable_api_capture: this.isApiCaptureEnabled, }; const dynamicUrl = [ @@ -136,13 +129,16 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone ENV.endpoints['dynamicscans'], ].join('/'); - await this.ajax.post(dynamicUrl, { namespace: ENV.namespace_v2, data }); + const res = await this.ajax.post(dynamicUrl, { + namespace: ENV.namespace_v2, + data, + }); - this.args.onClose(); + const normalized = this.store.normalize('dynamicscan', res); - this.file.setBootingStatus(); + this.args.onScanStart(this.store.push(normalized) as DynamicscanModel); - this.args.pollDynamicStatus(); + this.args.onClose(); this.notify.success(this.tStartingScan); } catch (error) { @@ -151,24 +147,6 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone this.args.file.setDynamicStatusNone(); } }); - - fetchAllAvailableManualDevices = task(async (manualDevices = true) => { - try { - const query = { - projectId: this.projectId, - manualDevices, - }; - - const availableDevices = await this.store.query( - 'project-available-device', - query - ); - - this.allAvailableManualDevices = availableDevices.slice(); - } catch (error) { - this.notify.error(parseError(error)); - } - }); } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.hbs b/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.hbs index 62b7004e4..a2302b326 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.hbs +++ b/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.hbs @@ -62,7 +62,7 @@ @placeholder={{t 'modalCard.dynamicScan.selectDevicePreference'}} @options={{this.deviceSelectionTypes}} @selected={{this.selectedDeviceSelection}} - @onChange={{@dpContext.handleDsManualDeviceSelection}} + @onChange={{this.handleDsManualDeviceSelectionChange}} {{style minWidth='330px'}} data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefSelect as |dst| @@ -75,8 +75,6 @@ {{/if}} @@ -101,8 +99,8 @@ diff --git a/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.ts b/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.ts index b228f8526..a59a667f9 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.ts +++ b/app/components/file-details/dynamic-scan/action/drawer/manual-dast/index.ts @@ -1,19 +1,18 @@ +import { action } from '@ember/object'; import Component from '@glimmer/component'; import ENUMS from 'irene/enums'; -import type { DevicePreferenceContext } from 'irene/components/project-preferences/provider'; -import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; import type FileModel from 'irene/models/file'; +import type { DsPreferenceContext } from 'irene/components/ds-preference-provider'; +import type DsManualDevicePreferenceModel from 'irene/models/ds-manual-device-preference'; export interface FileDetailsDynamicScanDrawerManualDastSignature { Element: HTMLElement; Args: { file: FileModel; - dpContext: DevicePreferenceContext; - isApiScanEnabled: boolean; - enableApiScan(event: MouseEvent, checked?: boolean): void; - allAvailableManualDevices: ProjectAvailableDeviceModel[]; - isFetchingManualDevices: boolean; + dpContext: DsPreferenceContext; + isApiCaptureEnabled: boolean; + onApiCaptureChange(event: Event, checked?: boolean): void; }; } @@ -24,9 +23,16 @@ export default class FileDetailsDynamicScanDrawerManualDastComponent extends Com return this.args.file; } + get dpContext() { + return this.args.dpContext; + } + + get devicePreference() { + return this.dpContext.dsManualDevicePreference; + } + get manualDeviceSelection() { - return this.args.dpContext?.dsManualDevicePreference - ?.ds_manual_device_selection; + return this.devicePreference?.dsManualDeviceSelection; } get selectedDeviceSelection() { @@ -53,6 +59,17 @@ export default class FileDetailsDynamicScanDrawerManualDastComponent extends Com get deviceDisplay() { return this.file.project.get('platformDisplay'); } + + @action + handleDsManualDeviceSelectionChange(opt: { value: number }) { + const preference = this.devicePreference as DsManualDevicePreferenceModel; + + preference.set('dsManualDeviceSelection', opt.value); + + if (opt.value !== ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE) { + this.dpContext.updateDsManualDevicePref(preference); + } + } } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/file-details/dynamic-scan/action/expiry/index.ts b/app/components/file-details/dynamic-scan/action/expiry/index.ts index f60dc9bff..26bdcdabb 100644 --- a/app/components/file-details/dynamic-scan/action/expiry/index.ts +++ b/app/components/file-details/dynamic-scan/action/expiry/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable ember/no-observers */ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { task } from 'ember-concurrency'; @@ -6,20 +5,18 @@ import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { later } from '@ember/runloop'; import { EmberRunTimer } from '@ember/runloop/types'; -import { addObserver, removeObserver } from '@ember/object/observers'; import type Store from '@ember-data/store'; import { Duration } from 'dayjs/plugin/duration'; import dayjs from 'dayjs'; -import type FileModel from 'irene/models/file'; import type DatetimeService from 'irene/services/datetime'; import type DynamicscanModel from 'irene/models/dynamicscan'; import ENV from 'irene/config/environment'; export interface DynamicScanExpirySignature { Args: { - file: FileModel; + dynamicscan: DynamicscanModel; }; } @@ -28,7 +25,6 @@ export default class DynamicScanExpiryComponent extends Component { - this.clock(); - }); + this.clock(); } willDestroy() { super.willDestroy(); this.clockStop = true; - - if (this.dynamicscan) { - removeObserver( - this.dynamicscan, - 'isReadyOrRunning', - this.observeDeviceState - ); - } } get extendTimeOptions() { return [5, 15, 30]; } - get profileId() { - return this.args.file.profile.get('id'); - } - - fetchDynamicscan = task(async () => { - if (this.profileId) { - this.dynamicscan = await this.store.findRecord( - 'dynamicscan', - this.profileId - ); - } - - if (this.dynamicscan) { - addObserver( - this.dynamicscan, - 'isReadyOrRunning', - this.observeDeviceState - ); - } - }); - - observeDeviceState() { - this.fetchDynamicscan.perform(); + get dynamicscan() { + return this.args.dynamicscan; } get canExtend() { @@ -97,7 +62,7 @@ export default class DynamicScanExpiryComponent extends Component { - const dynamicscan = this.dynamicscan; - - if (!dynamicscan) { + if (!this.dynamicscan) { return; } try { - await dynamicscan.extendTime(time); + await this.dynamicscan.extendTime(time); + + await this.dynamicscan.reload(); } catch (error) { const err = error as AdapterError; @@ -158,8 +125,6 @@ export default class DynamicScanExpiryComponent extends Component - {{! TODO: Logic should be replaced by comments when full DAST feature is ready }} - {{!-- {{#if @dynamicScan.isReadyOrRunning}} --}} - {{#if (or @file.isDynamicStatusReady @file.isDynamicStatusInProgress)}} - {{#if @isAutomatedScan}} - - <:leftIcon> - {{#if this.dynamicShutdown.isRunning}} - - {{else}} - - {{/if}} - - - <:default>{{t 'cancelScan'}} - - {{else}} - - <:leftIcon> - {{#if this.dynamicShutdown.isRunning}} - - {{else}} - - {{/if}} - + + <:leftIcon> + + - <:default>{{t 'stop'}} - - {{/if}} + <:default> + {{this.dynamicScanActionButton.text}} + + - {{!-- {{else if (or @file.isDynamicDone @dynamicScan.isDynamicStatusError)}} --}} - {{else if (or @file.isDynamicDone @file.isDynamicStatusError)}} - - <:leftIcon> - - - - <:default>{{@dynamicScanText}} - - {{else}} - - <:leftIcon> - - - - <:default>{{@dynamicScanText}} - + + {{/if}} - - -{{#if this.showDynamicScanDrawer}} - -{{/if}} - -{{!-- {{#if this.showDynamicScanDrawer}} - - - -{{/if}} --}} \ No newline at end of file + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/action/index.ts b/app/components/file-details/dynamic-scan/action/index.ts index 965f5a987..a210e8dd0 100644 --- a/app/components/file-details/dynamic-scan/action/index.ts +++ b/app/components/file-details/dynamic-scan/action/index.ts @@ -3,40 +3,31 @@ import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { task } from 'ember-concurrency'; import { action } from '@ember/object'; +import type IntlService from 'ember-intl/services/intl'; -import ENUMS from 'irene/enums'; import ENV from 'irene/config/environment'; +import parseError from 'irene/utils/parse-error'; import triggerAnalytics from 'irene/utils/trigger-analytics'; import type FileModel from 'irene/models/file'; -import type PollService from 'irene/services/poll'; import type DynamicscanModel from 'irene/models/dynamicscan'; -import type { DevicePreferenceContext } from 'irene/components/project-preferences-old/provider'; export interface DynamicScanActionSignature { Args: { onScanShutdown?: () => void; + onScanStart: (dynamicscan: DynamicscanModel) => void; file: FileModel; dynamicScanText: string; isAutomatedScan?: boolean; dynamicScan: DynamicscanModel | null; - dpContext: DevicePreferenceContext; }; } export default class DynamicScanActionComponent extends Component { - @service declare ajax: any; + @service declare intl: IntlService; @service('notifications') declare notify: NotificationService; - @service declare poll: PollService; - @service('browser/window') declare window: Window; @tracked showDynamicScanDrawer = false; - constructor(owner: unknown, args: DynamicScanActionSignature['Args']) { - super(owner, args); - - this.pollDynamicStatus(); - } - get file() { return this.args.file; } @@ -46,7 +37,50 @@ export default class DynamicScanActionComponent extends Component this.dynamicShutdown.perform(), + loading: this.dynamicShutdown.isRunning, + }; + } + + if (this.args.dynamicScan?.isReadyOrRunning) { + return { + icon: 'stop-circle', + text: this.intl.t('stop'), + testId: 'stopBtn', + loading: this.dynamicShutdown.isRunning, + onClick: () => this.dynamicShutdown.perform(), + }; + } + + if ( + this.args.dynamicScan?.isCompleted || + this.args.dynamicScan?.isStatusError + ) { + return { + icon: 'refresh', + text: this.args.dynamicScanText, + testId: 'restartBtn', + onClick: this.openDynamicScanDrawer, + }; + } + + return { + icon: 'play-arrow', + text: this.args.dynamicScanText, + testId: 'startBtn', + onClick: this.openDynamicScanDrawer, + }; } @action @@ -64,75 +98,13 @@ export default class DynamicScanActionComponent extends Component - this.file - ?.reload() - .then((f) => { - // Remove device preferences from local storage after start of dynamic scan - const { device_type, platform_version, file_id } = JSON.parse( - this.window.localStorage.getItem('actualDevicePrefData') ?? 'null' - ) as { - device_type: string | number | undefined; - platform_version: string; - file_id: string; - }; - - if (file_id && f.id === file_id && f.isDynamicStatusInProgress) { - this.args.dpContext.updateDevicePref( - device_type, - platform_version, - true - ); - - this.window.localStorage.removeItem('actualDevicePrefData'); - } - - // Stop polling - if ( - f.dynamicStatus === ENUMS.DYNAMIC_STATUS.NONE || - f.dynamicStatus === ENUMS.DYNAMIC_STATUS.READY - ) { - stopPoll(); - } - }) - .catch(() => stopPoll()), - 5000 - ); - } - - @action shutdownDynamicScan() { - this.dynamicShutdown.perform(); - this.args.onScanShutdown?.(); - } - dynamicShutdown = task({ drop: true }, async () => { - this.file.setShuttingDown(); - - const dynamicUrl = [ENV.endpoints['dynamic'], this.file.id].join('/'); - try { - await this.ajax.delete(dynamicUrl); + await this.args.dynamicScan?.destroyRecord(); - if (!this.isDestroyed) { - this.pollDynamicStatus(); - } + this.args.onScanShutdown?.(); } catch (error) { - this.file.setNone(); - - this.notify.error((error as AdapterError).payload.error); + this.notify.error(parseError(error, this.intl.t('pleaseTryAgain'))); } }); } diff --git a/app/components/file-details/dynamic-scan/automated/index.hbs b/app/components/file-details/dynamic-scan/automated/index.hbs index b93d06ea1..003c582c9 100644 --- a/app/components/file-details/dynamic-scan/automated/index.hbs +++ b/app/components/file-details/dynamic-scan/automated/index.hbs @@ -1,129 +1,131 @@ -{{#if this.fetchDynamicscan.isRunning}} - - +{{#if this.dynamicscanAutomationFeatureAvailable}} + {{#if (or this.getDynamicscanMode.isRunning this.fetchDynamicscan.isRunning)}} + + - - {{t 'loading'}}... - - -{{else if (and @file.canRunAutomatedDynamicscan this.automationEnabled)}} - + + {{t 'loading'}}... + + + {{else if this.automationPreference.dynamicScanAutomationEnabled}} - - {{t 'realDevice'}} - + + {{t 'realDevice'}} + - - + + - + {{#unless this.dynamicScan.isShuttingDown}} + + {{/unless}} - - + --}} + + + {{#if this.isFullscreenView}} + + + + {{else}} + + + + {{/if}} + {{else}} + + - {{#if this.isFullscreenView}} - - - + {{t 'toggleAutomatedDAST'}} + - {{else}} - - - - {{/if}} - - -{{else if (and @file.canRunAutomatedDynamicscan (not this.automationEnabled))}} - - - - - {{t 'toggleAutomatedDAST'}} - - - - {{! TODO: Get the correct text for this description }} - lorem ipsum dolor sit amet consectetur adipiscing - - - - {{t 'goToSettings'}} - - + {{! TODO: Get the correct text for this description }} + lorem ipsum dolor sit amet consectetur adipiscing + + + {{t 'goToSettings'}} + + + {{/if}} {{else}} diff --git a/app/components/file-details/dynamic-scan/automated/index.ts b/app/components/file-details/dynamic-scan/automated/index.ts index de36ead97..257bb0329 100644 --- a/app/components/file-details/dynamic-scan/automated/index.ts +++ b/app/components/file-details/dynamic-scan/automated/index.ts @@ -8,9 +8,12 @@ import type IntlService from 'ember-intl/services/intl'; import type RouterService from '@ember/routing/router-service'; import type Store from '@ember-data/store'; +import ENUMS from 'irene/enums'; import parseError from 'irene/utils/parse-error'; import type DynamicscanModel from 'irene/models/dynamicscan'; import type FileModel from 'irene/models/file'; +import type DsAutomationPreferenceModel from 'irene/models/ds-automation-preference'; +import type OrganizationService from 'irene/services/organization'; export interface FileDetailsDastAutomatedSignature { Args: { @@ -23,10 +26,11 @@ export default class FileDetailsDastAutomated extends Component { try { - const dynScanMode = await waitForPromise( - this.store.queryRecord('dynamicscan-mode', { - id: this.args.profileId, - }) - ); + const adapter = this.store.adapterFor('ds-automation-preference'); + adapter.setNestedUrlNamespace(String(this.args.profileId)); - this.automationEnabled = dynScanMode.dynamicscanMode === 'Automated'; + this.automationPreference = await this.store.queryRecord( + 'ds-automation-preference', + {} + ); } catch (error) { this.notify.error(parseError(error, this.intl.t('pleaseTryAgain'))); } }); fetchDynamicscan = task(async () => { - const id = this.args.profileId; + const file = this.args.file; try { - this.dynamicScan = await this.store.findRecord('dynamicscan', id); + this.dynamicScan = await file.getLastDynamicScan( + file.id, + ENUMS.DYNAMIC_MODE.AUTOMATED + ); } catch (e) { this.notify.error(parseError(e, this.intl.t('pleaseTryAgain'))); } diff --git a/app/components/file-details/dynamic-scan/header/index.ts b/app/components/file-details/dynamic-scan/header/index.ts index e6c216ba3..61968fe2d 100644 --- a/app/components/file-details/dynamic-scan/header/index.ts +++ b/app/components/file-details/dynamic-scan/header/index.ts @@ -1,17 +1,30 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; import type IntlService from 'ember-intl/services/intl'; import type RouterService from '@ember/routing/router-service'; import type Store from '@ember-data/store'; import type DynamicscanModel from 'irene/models/dynamicscan'; import type FileModel from 'irene/models/file'; +import type ConfigurationService from 'irene/services/configuration'; +import ENUMS from 'irene/enums'; +import parseError from 'irene/utils/parse-error'; + +interface TabItem { + id: string; + label: string; + route: string; + activeRoutes: string; + inProgress?: boolean; + count?: number; +} export interface FileDetailsDastHeaderSignature { Args: { file: FileModel; profileId: number; - dynamicScan: DynamicscanModel | null; }; } @@ -19,8 +32,17 @@ export default class FileDetailsDastHeader extends Component { + try { + this.dsAutomatedScan = await this.file.getLastDynamicScan( + this.file.id, + ENUMS.DYNAMIC_MODE.AUTOMATED + ); + } catch (error) { + this.notify.error(parseError(error)); + } + }); } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/file-details/dynamic-scan/index.hbs b/app/components/file-details/dynamic-scan/index.hbs index 83192f02b..06db0fb14 100644 --- a/app/components/file-details/dynamic-scan/index.hbs +++ b/app/components/file-details/dynamic-scan/index.hbs @@ -1,7 +1,3 @@ - + {{yield}} \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/index.ts b/app/components/file-details/dynamic-scan/index.ts index 8e74e430b..fd6bf94e5 100644 --- a/app/components/file-details/dynamic-scan/index.ts +++ b/app/components/file-details/dynamic-scan/index.ts @@ -1,12 +1,4 @@ import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; -import { task } from 'ember-concurrency'; -import { service } from '@ember/service'; -import type Store from '@ember-data/store'; -import type IntlService from 'ember-intl/services/intl'; - -import parseError from 'irene/utils/parse-error'; -import type DynamicscanModel from 'irene/models/dynamicscan'; import type FileModel from 'irene/models/file'; interface DynamicScanSignature { @@ -19,23 +11,7 @@ interface DynamicScanSignature { }; } -export default class DynamicScan extends Component { - @service declare store: Store; - @service declare intl: IntlService; - @service('notifications') declare notify: NotificationService; - - @tracked dynamicScan: DynamicscanModel | null = null; - - fetchDynamicscan = task(async () => { - const id = this.args.profileId; - - try { - this.dynamicScan = await this.store.findRecord('dynamicscan', id); - } catch (e) { - this.notify.error(parseError(e, this.intl.t('pleaseTryAgain'))); - } - }); -} +export default class DynamicScan extends Component {} declare module '@glint/environment-ember-loose/registry' { export default interface Registry { diff --git a/app/components/file-details/dynamic-scan/manual/index.hbs b/app/components/file-details/dynamic-scan/manual/index.hbs index 37ab8f3c8..3cdceca59 100644 --- a/app/components/file-details/dynamic-scan/manual/index.hbs +++ b/app/components/file-details/dynamic-scan/manual/index.hbs @@ -21,39 +21,42 @@ {{t 'realDevice'}} - - {{#if this.showStatusChip}} - + {{#if this.fetchDynamicscan.isRunning}} + + {{else}} + + {{#if this.showStatusChip}} + + {{/if}} - {{/if}} + {{#if this.showActionButton}} + + {{/if}} - {{#if this.showActionButton}} - - {{/if}} - - {{#if (and @file.isReady (not this.isFullscreenView))}} - - - - {{/if}} - + {{#if (and this.dynamicScan.isReady (not this.isFullscreenView))}} + + + + {{/if}} + + {{/if}}
{{/if}} diff --git a/app/components/file-details/dynamic-scan/manual/index.ts b/app/components/file-details/dynamic-scan/manual/index.ts index 17f4cadaf..f391eddd9 100644 --- a/app/components/file-details/dynamic-scan/manual/index.ts +++ b/app/components/file-details/dynamic-scan/manual/index.ts @@ -9,13 +9,12 @@ import type IntlService from 'ember-intl/services/intl'; import parseError from 'irene/utils/parse-error'; import type DynamicscanModel from 'irene/models/dynamicscan'; import type FileModel from 'irene/models/file'; -import type { DevicePreferenceContext } from 'irene/components/project-preferences-old/provider'; +import ENUMS from 'irene/enums'; export interface FileDetailsDastManualSignature { Args: { file: FileModel; profileId: number; - dpContext: DevicePreferenceContext; }; } @@ -32,35 +31,19 @@ export default class FileDetailsDastManual extends Component { - const id = this.args.profileId; + const file = this.args.file; try { - this.dynamicScan = await this.store.findRecord('dynamicscan', id); + this.dynamicScan = await file.getLastDynamicScan( + file.id, + ENUMS.DYNAMIC_MODE.MANUAL + ); } catch (e) { this.notify.error(parseError(e, this.intl.t('pleaseTryAgain'))); } diff --git a/app/components/file-details/dynamic-scan/status-chip/index.hbs b/app/components/file-details/dynamic-scan/status-chip/index.hbs index 3c3cc5ecd..ac47e8ad4 100644 --- a/app/components/file-details/dynamic-scan/status-chip/index.hbs +++ b/app/components/file-details/dynamic-scan/status-chip/index.hbs @@ -3,7 +3,7 @@ data-test-fileDetails-dynamicScan-statusChip @variant='semi-filled' @size='small' - @label={{if @chipStatusText @chipStatusText this.chipDetails.label}} + @label={{this.chipDetails.label}} @color={{this.chipDetails.color}} {{style textTransform='uppercase'}} ...attributes @@ -25,7 +25,7 @@ {{else}} { @service declare intl: IntlService; - get file() { - return this.args.file; + get status() { + return this.args.status; + } + + get statusText() { + return this.args.statusText; } get chipColor() { - return this.getColor(this.file?.dynamicStatus, false) as AkChipColor; + return this.getColor(this.status, false) as AkChipColor; } get loaderColor() { - return this.getColor(this.file?.dynamicStatus, true) as AkLoaderColor; + return this.getColor(this.status, true) as AkLoaderColor; } get chipDetails() { - if (this.file.isDynamicStatusError) { + if (this.status === DsComputedStatus.ERROR) { return { label: this.intl.t('errored'), color: 'error' as const, icon: 'warning', }; - } else if (this.file.isDynamicStatusInProgress) { + } else if (this.status === DsComputedStatus.IN_PROGRESS) { + return { + label: this.statusText || this.intl.t('inProgress'), + color: this.chipColor, + loaderColor: this.loaderColor, + }; + } else if (this.status === DsComputedStatus.RUNNING) { return { - label: this.file.statusText, + label: this.statusText || this.intl.t('running'), color: this.chipColor, loaderColor: this.loaderColor, }; - } else if (this.file.isDynamicDone) { + } else if (this.status === DsComputedStatus.COMPLETED) { return { label: this.intl.t('completed'), color: 'success' as const, }; + } else if (this.status === DsComputedStatus.CANCELLED) { + return { + label: this.intl.t('cancelled'), + color: 'secondary' as const, + }; } return { @@ -66,13 +76,13 @@ export default class DynamicScanStatusChipComponent extends Component - + {{#if this.fetchLatestManualAutomaticScan.isRunning}} + + {{else}} + + {{/if}} diff --git a/app/components/file-details/scan-actions/dynamic-scan/index.ts b/app/components/file-details/scan-actions/dynamic-scan/index.ts index 941fef9b0..5e626c4f5 100644 --- a/app/components/file-details/scan-actions/dynamic-scan/index.ts +++ b/app/components/file-details/scan-actions/dynamic-scan/index.ts @@ -1,8 +1,15 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; import type IntlService from 'ember-intl/services/intl'; import type FileModel from 'irene/models/file'; +import type DynamicscanModel from 'irene/models/dynamicscan'; +import { DsComputedStatus } from 'irene/models/dynamicscan'; +import ENUMS from 'irene/enums'; +import parseError from 'irene/utils/parse-error'; +import { action } from '@ember/object'; export interface FileDetailsScanActionsDynamicScanSignature { Args: { @@ -12,18 +19,82 @@ export interface FileDetailsScanActionsDynamicScanSignature { export default class FileDetailsScanActionsDynamicScanComponent extends Component { @service declare intl: IntlService; + @service('notifications') declare notify: NotificationService; - get chipStatusText() { - if (this.args.file.isDynamicStatusNeitherNoneNorReadyNorError) { - return this.args.file.statusText; - } else if (this.args.file.isDynamicStatusInProgress) { - return this.intl.t('inProgress'); - } else if (this.args.file.isDynamicDone) { - return this.intl.t('completed'); - } else { - return this.intl.t('notStarted'); + @tracked automatedDynamicScan: DynamicscanModel | null = null; + @tracked manualDynamicScan: DynamicscanModel | null = null; + + constructor( + owner: unknown, + args: FileDetailsScanActionsDynamicScanSignature['Args'] + ) { + super(owner, args); + + this.fetchLatestManualAutomaticScan.perform(); + } + + get status() { + const automatedStatus = this.automatedDynamicScan?.computedStatus; + const manualStatus = this.manualDynamicScan?.computedStatus; + + if (automatedStatus && manualStatus) { + return this.computeStatus(automatedStatus, manualStatus); } + + const singleStatus = automatedStatus || manualStatus; + + if (singleStatus === DsComputedStatus.RUNNING) { + return DsComputedStatus.IN_PROGRESS; + } + + return singleStatus || DsComputedStatus.NOT_STARTED; } + + @action + computeStatus(s1: DsComputedStatus, s2: DsComputedStatus) { + // If either scan has error, return error + if (s1 === DsComputedStatus.ERROR || s2 === DsComputedStatus.ERROR) { + return DsComputedStatus.ERROR; + } + + // If either scan is in progress, return in progress + if ( + s1 === DsComputedStatus.IN_PROGRESS || + s2 === DsComputedStatus.IN_PROGRESS || + s1 === DsComputedStatus.RUNNING || + s2 === DsComputedStatus.RUNNING + ) { + return DsComputedStatus.IN_PROGRESS; + } + + // If both scans are completed, return completed + if ( + s1 === DsComputedStatus.COMPLETED && + s2 === DsComputedStatus.COMPLETED + ) { + return DsComputedStatus.COMPLETED; + } + + return DsComputedStatus.NOT_STARTED; + } + + fetchLatestManualAutomaticScan = task(async () => { + try { + const file = this.args.file; + + this.automatedDynamicScan = await file.getLastDynamicScan( + file.id, + ENUMS.DYNAMIC_MODE.AUTOMATED + ); + + this.manualDynamicScan = await file.getLastDynamicScan( + file.id, + ENUMS.DYNAMIC_MODE.MANUAL + ); + } catch (error) { + this.notify.error(parseError(error)); + } + }); } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/novnc-rfb/index.ts b/app/components/novnc-rfb/index.ts index c4eb4ccfb..314000402 100644 --- a/app/components/novnc-rfb/index.ts +++ b/app/components/novnc-rfb/index.ts @@ -6,7 +6,7 @@ import RFB from '@novnc/novnc/lib/rfb'; export interface NovncRfbSignature { Args: { - deviceFarmURL: string; + deviceFarmURL: string | null; deviceFarmPassword: string; }; } @@ -43,3 +43,9 @@ export default class NovncRfbComponent extends Component { }); } } + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + NovncRfb: typeof NovncRfbComponent; + } +} diff --git a/app/components/project-preferences/device-preference/index.hbs b/app/components/project-preferences/device-preference/index.hbs deleted file mode 100644 index 4f274243a..000000000 --- a/app/components/project-preferences/device-preference/index.hbs +++ /dev/null @@ -1,68 +0,0 @@ - - {{#if (has-block 'title')}} - {{yield to='title'}} - {{else}} - - {{t 'devicePreferences'}} - - {{/if}} - - - {{t 'otherTemplates.selectPreferredDevice'}} - - - - -
- - {{t (device-type aks.value)}} - -
- -
- - {{#if (eq version '0')}} - {{t 'anyVersion'}} - {{else}} - {{version}} - {{/if}} - -
-
- -{{#if this.isPreferredDeviceNotAvailable}} - - - - - {{t 'modalCard.dynamicScan.preferredDeviceNotAvailable'}} - - -{{/if}} \ No newline at end of file diff --git a/app/components/project-preferences/device-preference/index.ts b/app/components/project-preferences/device-preference/index.ts deleted file mode 100644 index 093651bda..000000000 --- a/app/components/project-preferences/device-preference/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import Component from '@glimmer/component'; -import { DevicePreferenceContext } from '../provider'; - -export interface ProjectPreferencesDevicePreferenceSignature { - Args: { - dpContext: DevicePreferenceContext; - }; - Blocks: { - title: []; - }; -} - -export default class ProjectPreferencesDevicePreferenceComponent extends Component { - get isPreferredDeviceNotAvailable() { - return this.args.dpContext.isPreferredDeviceAvailable === false; - } -} - -declare module '@glint/environment-ember-loose/registry' { - export default interface Registry { - 'ProjectPreferences::DevicePreference': typeof ProjectPreferencesDevicePreferenceComponent; - 'project-preferences/device-preference': typeof ProjectPreferencesDevicePreferenceComponent; - } -} diff --git a/app/components/project-preferences/index.hbs b/app/components/project-preferences/index.hbs deleted file mode 100644 index 3e8656e94..000000000 --- a/app/components/project-preferences/index.hbs +++ /dev/null @@ -1,14 +0,0 @@ - - {{yield - (hash - DevicePreferenceComponent=(component - 'project-preferences/device-preference' dpContext=dpContext - ) - ) - }} - \ No newline at end of file diff --git a/app/components/project-preferences/index.ts b/app/components/project-preferences/index.ts deleted file mode 100644 index f56830e4b..000000000 --- a/app/components/project-preferences/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Component from '@glimmer/component'; -import { WithBoundArgs } from '@glint/template'; - -import ProjectModel from 'irene/models/project'; -import ProjectPreferencesDevicePreferenceComponent from './device-preference'; - -export interface ProjectPreferencesSignature { - Args: { - project?: ProjectModel | null; - profileId?: number | string; - platform?: number; - }; - Blocks: { - default: [ - { - DevicePreferenceComponent: WithBoundArgs< - typeof ProjectPreferencesDevicePreferenceComponent, - 'dpContext' - >; - }, - ]; - }; -} - -export default class ProjectPreferencesComponent extends Component {} - -declare module '@glint/environment-ember-loose/registry' { - export default interface Registry { - ProjectPreferences: typeof ProjectPreferencesComponent; - } -} diff --git a/app/components/project-preferences/provider/index.hbs b/app/components/project-preferences/provider/index.hbs deleted file mode 100644 index 6749e875f..000000000 --- a/app/components/project-preferences/provider/index.hbs +++ /dev/null @@ -1,22 +0,0 @@ -{{yield - (hash - deviceTypes=this.filteredDeviceTypes - selectedDeviceType=this.selectedDeviceType - handleSelectDeviceType=this.handleSelectDeviceType - selectedVersion=this.selectedVersion - devicePlatformVersions=this.devicePlatformVersionOptions - handleSelectVersion=this.handleSelectVersion - handleDsAutomatedMinOSVersionSelect=this.handleDsAutomatedMinOSVersionSelect - projectProfile=this.projectProfile - isPreferredDeviceAvailable=this.isPreferredDeviceAvailable - dsManualDevicePreference=this.dsManualDevicePreference - dsAutomatedDevicePreference=this.dsAutomatedDevicePreference - handleSelectDsManualIdentifier=this.handleSelectDsManualIdentifier - handleDsAutomatedDeviceSelection=this.handleDsAutomatedDeviceSelection - handleDsManualDeviceSelection=this.handleDsManualDeviceSelection - handleSelectDsAutomatedDeviceType=this.handleSelectDsAutomatedDeviceType - handleSelectDsAutomatedDeviceCapability=this.handleSelectDsAutomatedDeviceCapability - loadingDsAutoDevicePref=this.fetchDsAutomatedDevicePref.isRunning - loadingDsManualDevicePref=this.fetchDsManualDevicePref.isRunning - ) -}} \ No newline at end of file diff --git a/app/components/project-preferences/provider/index.ts b/app/components/project-preferences/provider/index.ts deleted file mode 100644 index 791d595b6..000000000 --- a/app/components/project-preferences/provider/index.ts +++ /dev/null @@ -1,432 +0,0 @@ -// eslint-disable-next-line ember/use-ember-data-rfc-395-imports -import type DS from 'ember-data'; - -import Component from '@glimmer/component'; -import { inject as service } from '@ember/service'; -import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; -import { task } from 'ember-concurrency'; - -import ENUMS from 'irene/enums'; -import ENV from 'irene/config/environment'; - -import type Store from '@ember-data/store'; -import type IntlService from 'ember-intl/services/intl'; - -import type ProjectModel from 'irene/models/project'; -import type DevicePreferenceModel from 'irene/models/device-preference'; -import type ProjectAvailableDeviceModel from 'irene/models/project-available-device'; - -import ProfileModel, { - type SetProfileDSAutomatedDevicePrefData, - type SetProfileDSManualDevicePrefData, - type ProfileDSAutomatedDevicePrefData, - type ProfileDSManualDevicePrefData, -} from 'irene/models/profile'; - -type ProfileDsSelectFuncHandler = (option: { value: number }) => void; - -export interface DevicePreferenceContext { - deviceTypes: DeviceType[]; - selectedDeviceType?: DeviceType; - selectedVersion: string; - devicePlatformVersions: string[]; - isPreferredDeviceAvailable: boolean | null; - projectProfile?: ProfileModel | null; - - dsAutomatedDevicePreference?: Partial; - dsManualDevicePreference?: Partial; - - handleSelectDeviceType: (deviceType: DeviceType) => void; - handleSelectVersion: (version: string) => void; - handleDsAutomatedMinOSVersionSelect: (version: string) => void; - handleSelectDsManualIdentifier(id: string): void; - handleSelectDsAutomatedDeviceCapability(event: Event, checked: boolean): void; - - handleDsManualDeviceSelection: ProfileDsSelectFuncHandler; - handleDsAutomatedDeviceSelection: ProfileDsSelectFuncHandler; - handleSelectDsAutomatedDeviceType: ProfileDsSelectFuncHandler; - - loadingDsAutoDevicePref: boolean; - loadingDsManualDevicePref: boolean; -} - -export interface ProjectPreferencesProviderSignature { - Args: { - project?: ProjectModel | null; - profileId?: number | string; - platform?: number; - }; - Blocks: { - default: [DevicePreferenceContext]; - }; -} - -type EnumObject = { key: string; value: number | string }; -type DeviceType = EnumObject; - -export default class ProjectPreferencesProviderComponent extends Component { - @service declare intl: IntlService; - @service declare ajax: any; - @service('notifications') declare notify: NotificationService; - @service declare store: Store; - - @tracked selectedVersion = '0'; - - @tracked selectedDeviceType?: DeviceType; - @tracked deviceTypes = ENUMS.DEVICE_TYPE.CHOICES; - @tracked devicePreference?: DevicePreferenceModel; - - @tracked projectProfile?: ProfileModel | null = null; - @tracked dsManualDevicePreference?: Partial; - - @tracked - dsManualDevicePreferenceCopy?: Partial; - - @tracked - dsAutomatedDevicePreference?: Partial; - - @tracked - dsAutomatedDevicePreferenceCopy?: Partial; - - @tracked - devices: DS.AdapterPopulatedRecordArray | null = - null; - - constructor( - owner: unknown, - args: ProjectPreferencesProviderSignature['Args'] - ) { - super(owner, args); - - this.fetchProjectProfile.perform(); - - // TODO: To be removed when new DAST APIs go live - this.fetchDevicePreference.perform(); - this.fetchDevices.perform(); - } - - get tAnyVersion() { - return this.intl.t('anyVersion'); - } - - get filteredDeviceTypes() { - return this.deviceTypes.filter( - (type) => ENUMS.DEVICE_TYPE.UNKNOWN !== type.value - ); - } - - get availableDevices() { - return this.devices?.filter( - (d) => d.platform === this.args.project?.get('platform') - ); - } - - get filteredDevices() { - return this.availableDevices?.filter((device) => { - switch (this.selectedDeviceType?.value) { - case ENUMS.DEVICE_TYPE.NO_PREFERENCE: - return true; - - case ENUMS.DEVICE_TYPE.TABLET_REQUIRED: - return device.isTablet; - - case ENUMS.DEVICE_TYPE.PHONE_REQUIRED: - return !device.isTablet; - - default: - return true; - } - }); - } - - get uniqueDevices() { - return this.filteredDevices?.uniqBy('platformVersion'); - } - - get devicePlatformVersionOptions() { - return ['0', ...(this.uniqueDevices?.map((d) => d.platformVersion) || [])]; - } - - get isPreferredDeviceAvailable() { - // check whether preferences & devices are resolved - if (this.devicePreference && this.uniqueDevices) { - const deviceType = Number(this.devicePreference.deviceType); - const version = this.devicePreference.platformVersion; - - // if both device type and os is any then return true - if (deviceType === 0 && version === '0') { - return true; - } - - // if os is any then return true - if (version === '0') { - return true; - } - - // check if preferred device type & os exists - return this.uniqueDevices.some((d) => { - // if only device type is any then just check version - if (deviceType === 0) { - return d.platformVersion === version; - } - - return ( - d.platformVersion === version && - (d.isTablet - ? deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED - : deviceType === ENUMS.DEVICE_TYPE.PHONE_REQUIRED) - ); - }); - } - - return null; - } - - @action - handleSelectDeviceType(deviceType: DeviceType) { - this.selectedDeviceType = deviceType; - this.selectedVersion = '0'; - - this.versionSelected.perform(); - } - - @action - handleSelectVersion(version: string) { - this.selectedVersion = version; - - this.versionSelected.perform(); - } - - @action - handleSelectDsManualIdentifier(id: string) { - const updatedPref = { - ...this.dsManualDevicePreference, - ds_manual_device_identifier: id, - }; - - this.dsManualDevicePreference = updatedPref; - - this.updateDsManualDevicePreference.perform(updatedPref); - } - - @action - handleDsManualDeviceSelection(opt: { value: number }) { - const updatedDsManualDevicePreference = { - ...this.dsManualDevicePreference, - ds_manual_device_selection: opt.value, - }; - - this.dsManualDevicePreference = updatedDsManualDevicePreference; - - if (opt.value === ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE) { - this.updateDsManualDevicePreference.perform({ - ...updatedDsManualDevicePreference, - ds_manual_device_identifier: '', - }); - } - } - - @action - handleDsAutomatedMinOSVersionSelect(opt: string) { - const updatedPref = { - ...this.dsAutomatedDevicePreference, - ds_automated_platform_version_min: opt, - }; - - this.dsAutomatedDevicePreference = updatedPref; - - this.updateDsAutomatedDevicePreference.perform(updatedPref); - } - - @action - handleSelectDsAutomatedDeviceCapability(event: Event, checked: boolean) { - const dctKey = (event.target as HTMLElement).id as keyof Pick< - ProfileDSAutomatedDevicePrefData, - | 'ds_automated_vpn_required' - | 'ds_automated_sim_required' - | 'ds_automated_pin_lock_required' - >; - - const updatedDsAutomatedDevicePreference = { - ...this.dsAutomatedDevicePreference, - [dctKey]: checked, - }; - - this.updateDsAutomatedDevicePreference.perform( - updatedDsAutomatedDevicePreference - ); - } - - @action - handleDsAutomatedDeviceSelection(option: { value: number }) { - const updatedPref = { - ...this.dsAutomatedDevicePreference, - ds_automated_device_selection: option.value, - }; - - this.dsAutomatedDevicePreference = updatedPref; - - this.updateDsAutomatedDevicePreference.perform(updatedPref); - } - - @action - handleSelectDsAutomatedDeviceType(option: { value: number }) { - const isAnyDeviceSelection = - option.value === ENUMS.DS_AUTOMATED_DEVICE_SELECTION.ANY_DEVICE; - - const autoDastDeviceType = - this.dsAutomatedDevicePreference?.ds_automated_device_type; - - const deviceType = - isAnyDeviceSelection || !autoDastDeviceType - ? ENUMS.DS_AUTOMATED_DEVICE_TYPE.NO_PREFERENCE - : autoDastDeviceType; - - this.updateDsAutomatedDevicePreference.perform({ - ...this.dsAutomatedDevicePreference, - ds_automated_device_type: deviceType, - }); - } - - fetchProjectProfile = task(async () => { - try { - const profile = await this.store.findRecord( - 'profile', - Number(this.args.profileId) - ); - - this.projectProfile = profile; - - this.fetchDsManualDevicePref.perform(); - this.fetchDsAutomatedDevicePref.perform(); - } catch (error) { - this.notify.error(this.intl.t('errorFetchingDevicePreferences')); - } - }); - - fetchDsManualDevicePref = task(async () => { - try { - const dsManualDevicePreference = - await this.projectProfile?.getDsManualDevicePreference(); - - this.dsManualDevicePreference = dsManualDevicePreference; - this.dsManualDevicePreferenceCopy = dsManualDevicePreference; - } catch (error) { - this.notify.error(this.intl.t('failedToUpdateDsManualDevicePref')); - } - }); - - updateDsManualDevicePreference = task( - async (data: SetProfileDSManualDevicePrefData) => { - try { - const dsManualDevicePreference = - await this.projectProfile?.setDSManualDevicePrefData(data); - - this.dsManualDevicePreference = dsManualDevicePreference; - this.dsManualDevicePreferenceCopy = dsManualDevicePreference; - - this.notify.success(this.intl.t('savedPreferences')); - } catch (error) { - this.dsManualDevicePreference = this.dsManualDevicePreferenceCopy; - - this.notify.error(this.intl.t('errorFetchingDsManualDevicePref')); - } - } - ); - - updateDsAutomatedDevicePreference = task( - async (data: SetProfileDSAutomatedDevicePrefData) => { - try { - const dsAutomatedDevicePreference = - await this.projectProfile?.setDSAutomatedDevicePrefData(data); - - this.dsAutomatedDevicePreference = dsAutomatedDevicePreference; - this.dsAutomatedDevicePreferenceCopy = dsAutomatedDevicePreference; - - this.notify.success(this.intl.t('savedPreferences')); - } catch (error) { - this.dsAutomatedDevicePreference = this.dsAutomatedDevicePreferenceCopy; - - this.notify.error(this.intl.t('failedToUpdateDsAutomatedDevicePref')); - } - } - ); - - fetchDsAutomatedDevicePref = task(async () => { - try { - const dsAutomatedDevicePref = - await this.projectProfile?.getDsAutomatedDevicePreference(); - - this.dsAutomatedDevicePreference = dsAutomatedDevicePref; - this.dsManualDevicePreferenceCopy = dsAutomatedDevicePref; - } catch (error) { - this.notify.error(this.intl.t('errorFetchingDsAutomatedDevicePref')); - } - }); - - versionSelected = task(async () => { - try { - const profileId = this.args.profileId; - - const devicePreferences = [ - ENV.endpoints['profiles'], - profileId, - ENV.endpoints['devicePreferences'], - ].join('/'); - - const data = { - device_type: this.selectedDeviceType?.value, - platform_version: this.selectedVersion, - }; - - await this.ajax.put(devicePreferences, { data }); - - if (!this.isDestroyed && this.devicePreference) { - this.devicePreference.deviceType = this.selectedDeviceType - ?.value as number; - - this.devicePreference.platformVersion = this.selectedVersion; - - this.notify.success(this.intl.t('savedPreferences')); - } - } catch (e) { - this.notify.error(this.intl.t('somethingWentWrong')); - } - }); - - fetchDevicePreference = task(async () => { - try { - this.devicePreference = await this.store.queryRecord( - 'device-preference', - { - id: this.args.profileId, - } - ); - - this.selectedDeviceType = this.filteredDeviceTypes.find( - (it) => it.value === this.devicePreference?.deviceType - ); - - this.selectedVersion = this.devicePreference.platformVersion; - } catch (error) { - this.notify.error(this.intl.t('errorFetchingDevicePreferences')); - } - }); - - fetchDevices = task(async () => { - try { - this.devices = await this.store.query('project-available-device', { - projectId: this.args.project?.get('id'), - }); - } catch (error) { - this.notify.error(this.intl.t('errorFetchingDevices')); - } - }); -} - -declare module '@glint/environment-ember-loose/registry' { - export default interface Registry { - 'ProjectPreferences::Provider': typeof ProjectPreferencesProviderComponent; - } -} diff --git a/app/components/project-settings/general-settings/device-preferences-automated-dast/index.hbs b/app/components/project-settings/general-settings/device-preferences-automated-dast/index.hbs index c53ae2219..761defd64 100644 --- a/app/components/project-settings/general-settings/device-preferences-automated-dast/index.hbs +++ b/app/components/project-settings/general-settings/device-preferences-automated-dast/index.hbs @@ -1,105 +1,103 @@ - - {{#let dpContext.dsAutomatedDevicePreference as |dsAutoDevicePref|}} - - {{#if (has-block 'title')}} - {{yield to='title'}} - {{else}} - - {{t 'devicePreferencesAutomatedDast'}} - - {{/if}} + + {{#if (has-block 'title')}} + {{yield to='title'}} + {{else}} + + {{t 'devicePreferencesAutomatedDast'}} + + {{/if}} - - {{t (ds-automated-device-pref dst.value)}} - + + {{t (ds-automated-device-pref dst.value)}} + - {{#if - (eq - dsAutoDevicePref.ds_automated_device_selection - this.filterDsAutomatedDeviceCriteria - ) - }} - - {{!-- - - {{t 'deviceType'}} - + {{#if this.filterAutomatedDeviceSelection}} + + + + {{t 'deviceType'}} + - {{#if dpContext}} - - - - + {{#if @dpContext.loadingDsAutomatedDevicePref}} + - - - - - {{else}} + + {{else}} + + + + - + + + - - {{/if}} - + + + + + {{/if}} + - --}} + - - - {{t 'minOSVersion'}} - + + + {{t 'minOSVersion'}} + - {{#if dpContext.loadingDsAutoDevicePref}} - + {{#if @dpContext.loadingDsAutomatedDevicePref}} + + {{else}} + + {{#if (eq version '')}} + {{t 'anyVersion'}} {{else}} - - {{version}} - + {{version}} {{/if}} - + + {{/if}} + - {{!-- + {{!-- @@ -145,8 +143,6 @@ /> {{/if}} --}} - - {{/if}} - {{/let}} - \ No newline at end of file + {{/if}} + \ No newline at end of file diff --git a/app/components/project-settings/general-settings/device-preferences-automated-dast/index.ts b/app/components/project-settings/general-settings/device-preferences-automated-dast/index.ts index bdd0e7a38..da1476434 100644 --- a/app/components/project-settings/general-settings/device-preferences-automated-dast/index.ts +++ b/app/components/project-settings/general-settings/device-preferences-automated-dast/index.ts @@ -1,15 +1,21 @@ import { action } from '@ember/object'; import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; -import ENUMS from 'irene/enums'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import ENUMS from 'irene/enums'; import type IntlService from 'ember-intl/services/intl'; +import type Store from '@ember-data/store'; import type ProjectModel from 'irene/models/project'; -import { type ProfileDSAutomatedDevicePrefData } from 'irene/models/profile'; +import type AvailableAutomatedDeviceModel from 'irene/models/available-automated-device'; +import type { DsPreferenceContext } from 'irene/components/ds-preference-provider'; +import type DsAutomatedDevicePreferenceModel from 'irene/models/ds-automated-device-preference'; export interface ProjectSettingsGeneralSettingsDevicePreferencesAutomatedDastSignature { Args: { project?: ProjectModel | null; + dpContext: DsPreferenceContext; }; Blocks: { title: []; @@ -18,49 +24,115 @@ export interface ProjectSettingsGeneralSettingsDevicePreferencesAutomatedDastSig export default class ProjectSettingsGeneralSettingsDevicePreferencesAutomatedDastComponent extends Component { @service declare intl: IntlService; + @service declare store: Store; + + @tracked availableAutomatedDevices: AvailableAutomatedDeviceModel[] = []; deviceSelectionTypes = ENUMS.DS_AUTOMATED_DEVICE_SELECTION.BASE_CHOICES; - filterDsAutomatedDeviceCriteria = - ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA; + constructor( + owner: unknown, + args: ProjectSettingsGeneralSettingsDevicePreferencesAutomatedDastSignature['Args'] + ) { + super(owner, args); - get isIOSApp() { - return this.args.project?.platform === ENUMS.PLATFORM.IOS; + this.fetchAvailableAutomatedDevicesTask.perform(); + } + + get dpContext() { + return this.args.dpContext; + } + + get devicePreference() { + return this.dpContext.dsAutomatedDevicePreference; } - // TODO: Values to be updated in the future when DAST is supported on prem get minOSVersionOptions() { - return this.isIOSApp ? ['13', '14', '15', '16'] : ['9', '10', '12', '13']; + const platformVersions = this.availableAutomatedDevices.map( + (it) => it.platformVersion + ); + + return ['', ...platformVersions.uniq()]; } - @action getChosenDeviceSelection(selectedDevice?: string | number) { + get isIOSApp() { + return this.args.project?.platform === ENUMS.PLATFORM.IOS; + } + + get chosenDeviceSelection() { return this.deviceSelectionTypes.find( - (st) => String(st.value) === String(selectedDevice) + (st) => + String(st.value) === + String(this.devicePreference?.dsAutomatedDeviceSelection) + ); + } + + get filterAutomatedDeviceSelection() { + return ( + this.devicePreference?.dsAutomatedDeviceSelection === + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA ); } + // get deviceCapabilitiesOptions() { + // return [ + // { + // label: this.intl.t('sim'), + // id: 'ds_automated_sim_required', + // checked: !!this.devicePreference?.dsAutomatedSimRequired, + // }, + // { + // label: this.intl.t('vpn'), + // id: 'ds_automated_vpn_required', + // checked: !!this.devicePreference?.dsAutomatedVpnRequired, + // }, + // { + // label: this.intl.t('pinLock'), + // id: 'ds_automated_pin_lock_required', + // checked: !!this.devicePreference?.dsAutomatedPinLockRequired, + // }, + // ]; + // } + @action - getDeviceCapabilitiesOptionsData( - dsAutoDevicePref?: Partial - ) { - return [ - { - label: this.intl.t('sim'), - id: 'ds_automated_sim_required', - checked: !!dsAutoDevicePref?.ds_automated_sim_required, - }, - { - label: this.intl.t('vpn'), - id: 'ds_automated_vpn_required', - checked: !!dsAutoDevicePref?.ds_automated_vpn_required, - }, - { - label: this.intl.t('pinLock'), - id: 'ds_automated_pin_lock_required', - checked: !!dsAutoDevicePref?.ds_automated_pin_lock_required, - }, - ]; + handleSelectDeviceType(event: Event, type: string) { + const preference = this + .devicePreference as DsAutomatedDevicePreferenceModel; + + preference.dsAutomatedDeviceType = Number(type); + + this.dpContext.updateDsAutomatedDevicePref(preference); + } + + @action + handleDsAutomatedMinOSVersionSelect(opt: string) { + const preference = this + .devicePreference as DsAutomatedDevicePreferenceModel; + + preference.dsAutomatedPlatformVersionMin = opt; + + this.dpContext.updateDsAutomatedDevicePref(preference); } + + @action + handleDsAutomatedDeviceSelection(option: { value: number }) { + const preference = this + .devicePreference as DsAutomatedDevicePreferenceModel; + + preference.dsAutomatedDeviceSelection = option.value; + + this.dpContext.updateDsAutomatedDevicePref(preference); + } + + fetchAvailableAutomatedDevicesTask = task(async () => { + const adapter = this.store.adapterFor('available-automated-device'); + + adapter.setNestedUrlNamespace(this.args.project?.id as string); + + const devices = await this.store.findAll('available-automated-device'); + + this.availableAutomatedDevices = devices.slice(); + }); } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/project-settings/general-settings/dynamicscan-automation-settings/index.hbs b/app/components/project-settings/general-settings/dynamicscan-automation-settings/index.hbs index d970af041..6e3fd88f7 100644 --- a/app/components/project-settings/general-settings/dynamicscan-automation-settings/index.hbs +++ b/app/components/project-settings/general-settings/dynamicscan-automation-settings/index.hbs @@ -25,49 +25,48 @@
- {{! TODO: Uncomment when feature is available }} - {{!-- {{#if @featureAvailable}} --}} - + {{#if @featureAvailable}} + - - - + - - - + + + + - - - {{t 'dynScanAutoSchedNote'}} - + + + {{t 'dynScanAutoSchedNote'}} + + - - {{!-- {{else}} + {{else}} - {{/if}} --}} + {{/if}} \ No newline at end of file diff --git a/app/components/project-settings/general-settings/dynamicscan-automation-settings/index.ts b/app/components/project-settings/general-settings/dynamicscan-automation-settings/index.ts index 21a1f8cc6..33113bfa9 100644 --- a/app/components/project-settings/general-settings/dynamicscan-automation-settings/index.ts +++ b/app/components/project-settings/general-settings/dynamicscan-automation-settings/index.ts @@ -2,13 +2,12 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { task } from 'ember-concurrency'; -import Store from '@ember-data/store'; -import IntlService from 'ember-intl/services/intl'; -import { waitForPromise } from '@ember/test-waiters'; +import type Store from '@ember-data/store'; +import type IntlService from 'ember-intl/services/intl'; -import ENV from 'irene/config/environment'; -import ProjectModel from 'irene/models/project'; import parseError from 'irene/utils/parse-error'; +import type ProjectModel from 'irene/models/project'; +import type DsAutomationPreferenceModel from 'irene/models/ds-automation-preference'; export interface ProjectSettingsGeneralSettingsDyanmicscanAutomationSettingsSignature { Args: { @@ -21,10 +20,9 @@ export interface ProjectSettingsGeneralSettingsDyanmicscanAutomationSettingsSign export default class ProjectSettingsGeneralSettingsDyanmicscanAutomationSettingsComponent extends Component { @service declare store: Store; @service declare intl: IntlService; - @service declare ajax: any; @service('notifications') declare notify: NotificationService; - @tracked automationEnabled = false; + @tracked automationPreference: DsAutomationPreferenceModel | null = null; constructor( owner: unknown, @@ -57,58 +55,36 @@ export default class ProjectSettingsGeneralSettingsDyanmicscanAutomationSettings getDynamicscanMode = task(async () => { try { - const dynScanMode = await waitForPromise( - this.store.queryRecord('dynamicscan-mode', { - id: this.profileId, - }) - ); + const adapter = this.store.adapterFor('ds-automation-preference'); + adapter.setNestedUrlNamespace(this.profileId as string); - this.automationEnabled = dynScanMode.dynamicscanMode === 'Automated'; + this.automationPreference = await this.store.queryRecord( + 'ds-automation-preference', + {} + ); } catch (error) { this.notify.error(parseError(error, this.tPleaseTryAgain)); } }); - toggleDynamicscanMode = task(async () => { + toggleDynamicscanMode = task(async (_: Event, enabled: boolean) => { try { - this.automationEnabled = !this.automationEnabled; + this.automationPreference?.set('dynamicScanAutomationEnabled', enabled); - const dynamicscanMode = [ - ENV.endpoints['profiles'], - this.profileId, - ENV.endpoints['dynamicscanMode'], - ].join('/'); + const adapter = this.store.adapterFor('ds-automation-preference'); + adapter.setNestedUrlNamespace(this.profileId as string); - const data = { - dynamicscan_mode: this.automationEnabled ? 'Automated' : 'Manual', - }; + await this.automationPreference?.save(); - await waitForPromise(this.ajax.put(dynamicscanMode, { data })); - - const successMsg = this.automationEnabled + const successMsg = enabled ? this.tAppiumScheduledAutomationSuccessOn : this.tAppiumScheduledAutomationSuccessOff; this.notify.success(successMsg); } catch (err) { - const error = err as AdapterError; - this.automationEnabled = !this.automationEnabled; - - if (error.payload) { - Object.keys(error.payload).forEach((p) => { - let errMsg = error.payload[p]; - - if (typeof errMsg !== 'string') { - errMsg = error.payload[p][0]; - } - - this.notify.error(errMsg); - }); - - return; - } + this.automationPreference?.rollbackAttributes(); - this.notify.error(parseError(error, this.tSomethingWentWrong)); + this.notify.error(parseError(err, this.tSomethingWentWrong)); } }); } diff --git a/app/components/project-settings/general-settings/index.hbs b/app/components/project-settings/general-settings/index.hbs index 3f51459e9..9fcae2a8a 100644 --- a/app/components/project-settings/general-settings/index.hbs +++ b/app/components/project-settings/general-settings/index.hbs @@ -5,25 +5,6 @@ local-class='general-settings-root' data-test-projectSettings-generalSettings-root > - - - - <:title> - - {{t 'devicePreferences'}} - - - - - - - - @@ -86,24 +67,34 @@ /> - - - - - + {{#unless this.orgIsAnEnterprise}} + - {{! TODO: uncomment when Automated DAST is ready}} - {{!-- - + + + - - --}} + {{#if this.dynamicscanAutomationFeatureAvailable}} + + + + + + + + {{/if}} + {{/unless}} diff --git a/app/components/project-settings/general-settings/index.ts b/app/components/project-settings/general-settings/index.ts index 87e77f5ab..df7d8d3da 100644 --- a/app/components/project-settings/general-settings/index.ts +++ b/app/components/project-settings/general-settings/index.ts @@ -2,13 +2,14 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { task } from 'ember-concurrency'; -import Store from '@ember-data/store'; import { waitForPromise } from '@ember/test-waiters'; +import type Store from '@ember-data/store'; -import MeService from 'irene/services/me'; -import ProjectModel from 'irene/models/project'; -import OrganizationService from 'irene/services/organization'; -import ProfileModel from 'irene/models/profile'; +import type ProjectModel from 'irene/models/project'; +import type ProfileModel from 'irene/models/profile'; +import type MeService from 'irene/services/me'; +import type OrganizationService from 'irene/services/organization'; +import type ConfigurationService from 'irene/services/configuration'; interface ProjectSettingsGeneralSettingsSignature { Args: { @@ -19,6 +20,7 @@ interface ProjectSettingsGeneralSettingsSignature { export default class ProjectSettingsGeneralSettingsComponent extends Component { @service declare me: MeService; @service declare organization: OrganizationService; + @service declare configuration: ConfigurationService; @service declare store: Store; @tracked profile: ProfileModel | null = null; @@ -36,10 +38,14 @@ export default class ProjectSettingsGeneralSettingsComponent extends Component

{ try { const profileId = this.args.project?.activeProfileId; diff --git a/app/components/vnc-viewer/index.hbs b/app/components/vnc-viewer/index.hbs index 1fa4939a2..577ca498c 100644 --- a/app/components/vnc-viewer/index.hbs +++ b/app/components/vnc-viewer/index.hbs @@ -5,15 +5,14 @@ @direction='column' @spacing='3' > - {{!-- {{#if @dynamicScan.isDynamicStatusInProgress}} --}} - {{#if @file.isReady}} + {{#if @dynamicScan.isReady}} - + {{#if (has-block 'controls')}} {{yield to='controls'}} @@ -21,19 +20,28 @@ {{/if}} - {{#if @file.isDynamicStatusStarting}} - + {{#if (and (not this.isAutomated) @dynamicScan.isStarting)}} + {{t 'note'}} - {{t 'dynamicScanText'}} {{/if}} - {{#if (and this.isAutomated @dynamicScan.isDynamicStatusInProgress)}} - + {{#if + (and + this.isAutomated + (or @dynamicScan.isStartingOrShuttingInProgress @dynamicScan.isRunning) + ) + }} + {{#if this.startedBy}} - + {{t 'scanStartedBy'}} {{this.startedBy}} @@ -60,13 +68,13 @@ {{/if}} {{/if}} -

- {{!-- {{#if (and @dynamicScan.isReadyOrRunning this.deviceType)}} --}} - {{#if (and @file.isReady this.deviceType)}} - {{#if this.isAutomated}} +
+ {{#if this.isAutomated}} + {{#if + (or + @dynamicScan.isRunning @dynamicScan.isStartingOrShuttingInProgress + ) + }} - - {{else}} + {{else if @dynamicScan.isReady}} { constructor(owner: unknown, args: VncViewerSignature['Args']) { super(owner, args); - this.fetchDevicePreference.perform(); + // TODO: get device details based on preference + // this.fetchDevicePreference.perform(); } get deviceFarmURL() { - const token = this.args.file.deviceToken; + const token = this.args.dynamicScan?.moriartyDynamicscanToken; - return this.devicefarm.getTokenizedWSURL(token); - } - - fetchDevicePreference = task(async () => { - const profileId = this.args.profileId; - - if (profileId) { - this.devicePreference = await this.store.queryRecord( - 'device-preference', - { id: profileId } - ); + if (token) { + return this.devicefarm.getTokenizedWSURL(token); } - }); - get screenRequired() { - const platform = this.args.file.project.get('platform'); - const deviceType = this.devicePreference?.deviceType; - - return ( - platform === ENUMS.PLATFORM.ANDROID && - deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED - ); + return null; } get deviceType() { const platform = this.args.file.project.get('platform'); - const deviceType = this.devicePreference?.deviceType; if (platform === ENUMS.PLATFORM.ANDROID) { - if (deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { - return 'tablet'; - } else { - return 'nexus5'; - } + return 'nexus5'; } else if (platform === ENUMS.PLATFORM.IOS) { - if (deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { - return 'ipad black'; - } else { - return 'iphone5s black'; - } + return 'iphone5s black'; } return ''; } + // get deviceType() { + // const platform = this.args.file.project.get('platform'); + // const deviceType = this.devicePreference?.deviceType; + + // if (platform === ENUMS.PLATFORM.ANDROID) { + // if (deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { + // return 'tablet'; + // } else { + // return 'nexus5'; + // } + // } else if (platform === ENUMS.PLATFORM.IOS) { + // if (deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { + // return 'ipad black'; + // } else { + // return 'iphone5s black'; + // } + // } + + // return ''; + // } + get isAutomated() { return this.args.isAutomated; } @@ -113,9 +108,7 @@ export default class VncViewerComponent extends Component { } get startedBy() { - const startedBy = this.dynamicScan?.startedByUser.get('username'); - - return startedBy; + return this.dynamicScan?.startedByUser?.get('username'); } @action diff --git a/app/controllers/authenticated/dashboard/file/dynamic-scan/automated.ts b/app/controllers/authenticated/dashboard/file/dynamic-scan/automated.ts new file mode 100644 index 000000000..6fe3aa2f2 --- /dev/null +++ b/app/controllers/authenticated/dashboard/file/dynamic-scan/automated.ts @@ -0,0 +1,48 @@ +import Controller from '@ember/controller'; +import { service } from '@ember/service'; + +import type IntlService from 'ember-intl/services/intl'; +import type FileModel from 'irene/models/file'; +import { type AkBreadcrumbsItemProps } from 'irene/services/ak-breadcrumbs'; + +export default class AuthenticatedDashboardFileDynamicScanAutomatedController extends Controller { + @service declare intl: IntlService; + + declare model: { file: FileModel; profileId: number }; + + get breadcrumbs(): AkBreadcrumbsItemProps { + const routeModels = [this.model?.file?.id]; + + const crumb: AkBreadcrumbsItemProps = { + title: this.intl.t('dastTabs.automatedDAST'), + route: 'authenticated.dashboard.file.dynamic-scan.automated', + models: routeModels, + routeGroup: 'project/files', + + siblingRoutes: [ + 'authenticated.dashboard.file.dynamic-scan.results', + 'authenticated.dashboard.file.dynamic-scan.manual', + ], + }; + + const parentCrumb: AkBreadcrumbsItemProps['parentCrumb'] = { + title: this.intl.t('scanDetails'), + route: 'authenticated.dashboard.file', + models: routeModels, + routeGroup: 'project/files', + }; + + return { + ...crumb, + parentCrumb, + fallbackCrumbs: [ + { + title: this.intl.t('allProjects'), + route: 'authenticated.dashboard.projects', + }, + parentCrumb, + crumb, + ], + }; + } +} diff --git a/app/controllers/authenticated/dashboard/file/dynamic-scan/manual.ts b/app/controllers/authenticated/dashboard/file/dynamic-scan/manual.ts index dad4cb67a..04345490d 100644 --- a/app/controllers/authenticated/dashboard/file/dynamic-scan/manual.ts +++ b/app/controllers/authenticated/dashboard/file/dynamic-scan/manual.ts @@ -14,7 +14,7 @@ export default class AuthenticatedDashboardFileDynamicScanManualController exten const routeModels = [this.model?.file?.id]; const crumb: AkBreadcrumbsItemProps = { - title: this.intl.t('dast'), + title: this.intl.t('dastTabs.manualDAST'), route: 'authenticated.dashboard.file.dynamic-scan.manual', models: routeModels, routeGroup: 'project/files', diff --git a/app/enums.ts b/app/enums.ts index ed5fac080..23c5515b3 100644 --- a/app/enums.ts +++ b/app/enums.ts @@ -55,6 +55,36 @@ const ENUMS = { RUNNING: 10, // TODO: check with backend after api is ready }, + DYNAMIC_SCAN_STATUS: { + NOT_STARTED: 0, + PREPROCESSING: 1, + PROCESSING_SCAN_REQUEST: 2, + IN_QUEUE: 3, + DEVICE_ALLOCATED: 4, + CONNECTING_TO_DEVICE: 5, + PREPARING_DEVICE: 6, + INSTALLING: 7, + CONFIGURING_API_CAPTURE: 8, + HOOKING: 9, + LAUNCHING: 10, + READY_FOR_INTERACTION: 11, + DOWNLOADING_AUTO_SCRIPT: 12, + CONFIGURING_AUTO_INTERACTION: 13, + INITIATING_AUTO_INTERACTION: 14, + AUTO_INTERACTION_COMPLETED: 15, + STOP_SCAN_REQUESTED: 16, + SCAN_TIME_LIMIT_EXCEEDED: 17, + SHUTTING_DOWN: 18, + CLEANING_DEVICE: 19, + RUNTIME_DETECTION_COMPLETED: 20, + ANALYZING: 21, + ANALYSIS_COMPLETED: 22, + TIMED_OUT: 23, + ERROR: 24, + CANCELLED: 25, + TERMINATED: 26, + }, + DYNAMIC_MODE: { MANUAL: 0, AUTOMATED: 1, diff --git a/app/models/api-scan-options.ts b/app/models/api-scan-options.ts index 897b74252..9d20fae5c 100644 --- a/app/models/api-scan-options.ts +++ b/app/models/api-scan-options.ts @@ -1,6 +1,4 @@ -/* eslint-disable ember/no-computed-properties-in-native-classes */ import Inflector from 'ember-inflector'; -import { computed } from '@ember/object'; import { isEmpty } from '@ember/utils'; import Model, { attr } from '@ember-data/model'; @@ -11,7 +9,6 @@ export default class ApiScanOptionsModel extends Model { @attr('string') declare apiUrlFilters: string; - @computed('apiUrlFilters') get apiUrlFilterItems() { if (!isEmpty(this.apiUrlFilters)) { return this.apiUrlFilters != null diff --git a/app/models/available-automated-device.ts b/app/models/available-automated-device.ts new file mode 100644 index 000000000..9ab15f85c --- /dev/null +++ b/app/models/available-automated-device.ts @@ -0,0 +1,9 @@ +import DeviceModel from './device'; + +export default class AvailableAutomatedDeviceModel extends DeviceModel {} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'available-automated-device': AvailableAutomatedDeviceModel; + } +} diff --git a/app/models/available-manual-device.ts b/app/models/available-manual-device.ts new file mode 100644 index 000000000..d5404247e --- /dev/null +++ b/app/models/available-manual-device.ts @@ -0,0 +1,9 @@ +import DeviceModel from './device'; + +export default class AvailableManualDeviceModel extends DeviceModel {} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'available-manual-device': AvailableManualDeviceModel; + } +} diff --git a/app/models/device.ts b/app/models/device.ts index c1bed7a25..7a2fe60c9 100644 --- a/app/models/device.ts +++ b/app/models/device.ts @@ -1,14 +1,65 @@ import Model, { attr } from '@ember-data/model'; export default class DeviceModel extends Model { + @attr('string') + declare state: string; + + @attr('string') + declare deviceIdentifier: string; + + @attr('string') + declare address: string; + + @attr('boolean') + declare isConnected: boolean; + + @attr('boolean') + declare isActive: boolean; + @attr('boolean') declare isTablet: boolean; - @attr('string') - declare version: string; + @attr('boolean') + declare isReserved: boolean; @attr('number') declare platform: number; + + @attr('string') + declare platformVersion: string; + + @attr('string') + declare cpuArchitecture: string; + + @attr('string') + declare model: string; + + @attr('boolean') + declare hasSim: boolean; + + @attr('string') + declare simNetwork: string; + + @attr('string') + declare simPhoneNumber: string; + + @attr('boolean') + declare hasPinLock: boolean; + + @attr('boolean') + declare hasVpn: boolean; + + @attr('string') + declare vpnPackageName: string; + + @attr('boolean') + declare hasPersistentApps: boolean; + + @attr() + declare persistentApps: unknown[]; + + @attr('boolean') + declare hasVnc: boolean; } declare module 'ember-data/types/registries/model' { diff --git a/app/models/ds-automated-device-preference.ts b/app/models/ds-automated-device-preference.ts new file mode 100644 index 000000000..2b09c7c04 --- /dev/null +++ b/app/models/ds-automated-device-preference.ts @@ -0,0 +1,30 @@ +import Model, { attr } from '@ember-data/model'; + +export default class DsAutomatedDevicePreferenceModel extends Model { + @attr('number') + declare dsAutomatedDeviceSelection: number; + + @attr('string') + declare dsAutomatedDeviceSelectionDisplay: string; + + @attr('number') + declare dsAutomatedDeviceType: number; + + @attr('string') + declare dsAutomatedPlatformVersionMin: string; + + @attr('string') + declare dsAutomatedPlatformVersionMax: string; + + @attr('string') + declare dsAutomatedDeviceIdentifier: string; + + @attr('boolean', { allowNull: true }) + declare dsAutomatedUseReservedDevice: boolean | null; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'ds-automated-device-preference': DsAutomatedDevicePreferenceModel; + } +} diff --git a/app/models/ds-automation-preference.ts b/app/models/ds-automation-preference.ts new file mode 100644 index 000000000..8daf99b27 --- /dev/null +++ b/app/models/ds-automation-preference.ts @@ -0,0 +1,12 @@ +import Model, { attr } from '@ember-data/model'; + +export default class DsAutomationPreferenceModel extends Model { + @attr('boolean') + declare dynamicScanAutomationEnabled: boolean; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'ds-automation-preference': DsAutomationPreferenceModel; + } +} diff --git a/app/models/ds-manual-device-preference.ts b/app/models/ds-manual-device-preference.ts new file mode 100644 index 000000000..ab78cfd2d --- /dev/null +++ b/app/models/ds-manual-device-preference.ts @@ -0,0 +1,18 @@ +import Model, { attr } from '@ember-data/model'; + +export default class DsManualDevicePreferenceModel extends Model { + @attr('number') + declare dsManualDeviceSelection: number; + + @attr('string') + declare dsManualDeviceSelectionDisplay: string; + + @attr('string') + declare dsManualDeviceIdentifier: string; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'ds-manual-device-preference': DsManualDevicePreferenceModel; + } +} diff --git a/app/models/dynamicscan.ts b/app/models/dynamicscan.ts index de5533329..261cc9aaa 100644 --- a/app/models/dynamicscan.ts +++ b/app/models/dynamicscan.ts @@ -1,12 +1,20 @@ -import Model, { attr, belongsTo, AsyncBelongsTo } from '@ember-data/model'; -import UserModel from './user'; -import ENUMS from 'irene/enums'; -// import DevicePreferenceModel from './device-preference'; -import AvailableDeviceModel from './available-device'; -import ScanParameterModel from './scan-parameter'; +import Model, { attr, belongsTo, type AsyncBelongsTo } from '@ember-data/model'; import { inject as service } from '@ember/service'; -import IntlService from 'ember-intl/services/intl'; -import FileModel from './file'; +import type IntlService from 'ember-intl/services/intl'; + +import ENUMS from 'irene/enums'; +import type UserModel from './user'; +import type FileModel from './file'; +import type DeviceModel from './device'; + +export enum DsComputedStatus { + NOT_STARTED, + IN_PROGRESS, + RUNNING, + COMPLETED, + CANCELLED, + ERROR, +} export default class DynamicscanModel extends Model { @service declare intl: IntlService; @@ -15,84 +23,57 @@ export default class DynamicscanModel extends Model { @belongsTo('file', { async: true, inverse: null }) declare file: AsyncBelongsTo; - @attr('number') - declare mode: number; - - @attr('number') - declare status: number; - - // User actions - @belongsTo('user', { async: true, inverse: null }) - declare startedByUser: AsyncBelongsTo; - - @belongsTo('user', { async: true, inverse: null }) - declare stoppedByUser: AsyncBelongsTo; - - // Scan user preferences - // @belongsTo('device-preference') - // declare devicePreference: AsyncBelongsTo; + @attr('string') + declare packageName: string; @attr('number') - declare deviceType: number; + declare mode: number; @attr('string') - declare platformVersion: string; - - @belongsTo('scan-parameter-group', { async: true, inverse: null }) - declare scanParameterGroups: AsyncBelongsTo; + declare modeDisplay: string; - @attr('boolean') - declare enableApiCapture: boolean; - - @attr() - declare apiCaptureFilters: unknown; //TODO: Check this type + @attr('number') + declare status: number; @attr('string') - declare proxyHost: string; + declare statusDisplay: string; @attr('string') - declare proxyPort: string; + declare moriartyDynamicscanrequestId: string; - // Devicefarm scan info @attr('string') declare moriartyDynamicscanId: string; @attr('string') declare moriartyDynamicscanToken: string; - @attr() - declare deviceUsed: unknown; //TODO: Check this type - - @attr('string') - declare errorCode: string; + // User actions + @belongsTo('user', { async: true, inverse: null }) + declare startedByUser: AsyncBelongsTo; - @attr('string') - declare errorMessage: string; + @belongsTo('user', { async: true, inverse: null }) + declare stoppedByUser: AsyncBelongsTo | null; @attr('date') declare createdOn: Date; - @attr('date') - declare updatedOn: Date; - - @attr('date') - declare endedOn: Date; + @attr('date', { allowNull: true }) + declare endedOn: Date | null; - @attr('date') - declare timeoutOn: Date; + @attr('date', { allowNull: true }) + declare autoShutdownOn: Date | null; - @attr('date') - declare autoShutdownOn: Date; + @belongsTo('device', { async: false, inverse: null }) + declare deviceUsed: DeviceModel | null; - // Post interaction - @attr('boolean') - declare isAnalysisDone: boolean; + @attr() + declare devicePreference: unknown; - @attr('number') - declare time: number; + @attr('string') + declare errorCode: string; - @belongsTo('available-device', { async: true, inverse: null }) - declare availableDevice: AsyncBelongsTo; + @attr('string') + declare errorMessage: string; async extendTime(time: number) { const adapter = this.store.adapterFor('dynamicscan'); @@ -132,167 +113,187 @@ export default class DynamicscanModel extends Model { this.setDynamicScanMode(ENUMS.DYNAMIC_MODE.AUTOMATED); } - setBootingStatus() { - this.setDynamicStatus(ENUMS.DYNAMIC_STATUS.BOOTING); - } - - setInQueueStatus() { - this.setDynamicStatus(ENUMS.DYNAMIC_STATUS.INQUEUE); - } - setShuttingDown() { - this.setDynamicStatus(ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN); + this.setDynamicStatus(ENUMS.DYNAMIC_SCAN_STATUS.STOP_SCAN_REQUESTED); } setNone() { - this.setDynamicStatus(ENUMS.DYNAMIC_STATUS.NONE); + this.setDynamicStatus(ENUMS.DYNAMIC_SCAN_STATUS.NOT_STARTED); } - setReady() { - this.setDynamicStatus(ENUMS.DYNAMIC_STATUS.READY); + get isNone() { + return this.status === ENUMS.DYNAMIC_SCAN_STATUS.NOT_STARTED; } - setRunning() { - this.setDynamicStatus(ENUMS.DYNAMIC_STATUS.RUNNING); + get isNotNone() { + return !this.isNone; } - get isReady() { - const status = this.status; - return status === ENUMS.DYNAMIC_STATUS.READY; + get isInqueue() { + return [ + ENUMS.DYNAMIC_SCAN_STATUS.PREPROCESSING, + ENUMS.DYNAMIC_SCAN_STATUS.PROCESSING_SCAN_REQUEST, + ENUMS.DYNAMIC_SCAN_STATUS.IN_QUEUE, + ].includes(this.status); } - get isDynamicStatusNone() { - const status = this.status; - return status === ENUMS.DYNAMIC_STATUS.NONE; + get isBooting() { + return [ + ENUMS.DYNAMIC_SCAN_STATUS.DEVICE_ALLOCATED, + ENUMS.DYNAMIC_SCAN_STATUS.CONNECTING_TO_DEVICE, + ENUMS.DYNAMIC_SCAN_STATUS.PREPARING_DEVICE, + ENUMS.DYNAMIC_SCAN_STATUS.DOWNLOADING_AUTO_SCRIPT, + ENUMS.DYNAMIC_SCAN_STATUS.CONFIGURING_AUTO_INTERACTION, + ].includes(this.status); } - get isDynamicStatusError() { - const status = this.status; - return status === ENUMS.DYNAMIC_STATUS.ERROR; + get isInstalling() { + return this.status === ENUMS.DYNAMIC_SCAN_STATUS.INSTALLING; } - get isDynamicStatusReady() { - const status = this.status; - return status === ENUMS.DYNAMIC_STATUS.READY; + get isLaunching() { + return this.status === ENUMS.DYNAMIC_SCAN_STATUS.LAUNCHING; } - get isDynamicStatusNotReady() { - return !this.isDynamicStatusReady; + get isHooking() { + return [ + ENUMS.DYNAMIC_SCAN_STATUS.CONFIGURING_API_CAPTURE, + ENUMS.DYNAMIC_SCAN_STATUS.HOOKING, + ].includes(this.status); } - get isDynamicStatusNotNone() { - return !this.isDynamicStatusNone; + get isReady() { + return this.status === ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION; } - get isDynamicStatusNeitherNoneNorReadyNorError() { - const status = this.status; - return ![ - ENUMS.DYNAMIC_STATUS.READY, - ENUMS.DYNAMIC_STATUS.NONE, - ENUMS.DYNAMIC_STATUS.ERROR, - ].includes(status); + get isNotReady() { + return !this.isReady; } - get isDynamicStatusNoneOrError() { - const status = this.status; - return [ENUMS.DYNAMIC_STATUS.NONE, ENUMS.DYNAMIC_STATUS.ERROR].includes( - status - ); + get isRunning() { + return [ + ENUMS.DYNAMIC_SCAN_STATUS.INITIATING_AUTO_INTERACTION, + ENUMS.DYNAMIC_SCAN_STATUS.AUTO_INTERACTION_COMPLETED, + ].includes(this.status); } - get isDynamicStatusNoneOrReady() { - const status = this.status; - return [ENUMS.DYNAMIC_STATUS.READY, ENUMS.DYNAMIC_STATUS.NONE].includes( - status - ); + get isShuttingDown() { + return [ + ENUMS.DYNAMIC_SCAN_STATUS.STOP_SCAN_REQUESTED, + ENUMS.DYNAMIC_SCAN_STATUS.SCAN_TIME_LIMIT_EXCEEDED, + ENUMS.DYNAMIC_SCAN_STATUS.SHUTTING_DOWN, + ENUMS.DYNAMIC_SCAN_STATUS.CLEANING_DEVICE, + ENUMS.DYNAMIC_SCAN_STATUS.RUNTIME_DETECTION_COMPLETED, + ].includes(this.status); } - get isReadyOrRunning() { - const status = this.status; - return [ENUMS.DYNAMIC_STATUS.READY, ENUMS.DYNAMIC_STATUS.RUNNING].includes( - status - ); + get isStatusError() { + return [ + ENUMS.DYNAMIC_SCAN_STATUS.ERROR, + ENUMS.DYNAMIC_SCAN_STATUS.TIMED_OUT, + ENUMS.DYNAMIC_SCAN_STATUS.TERMINATED, + ].includes(this.status); } - get isDynamicStatusStarting() { - const status = this.status; - return ![ - ENUMS.DYNAMIC_STATUS.READY, - ENUMS.DYNAMIC_STATUS.RUNNING, - ENUMS.DYNAMIC_STATUS.NONE, - ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN, - ].includes(status); + get isCompleted() { + return [ + ENUMS.DYNAMIC_SCAN_STATUS.ANALYZING, + ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + ].includes(this.status); } - get isDynamicStatusInProgress() { - const status = this.status; - return [ - ENUMS.DYNAMIC_STATUS.INQUEUE, - ENUMS.DYNAMIC_STATUS.BOOTING, - ENUMS.DYNAMIC_STATUS.DOWNLOADING, - ENUMS.DYNAMIC_STATUS.INSTALLING, - ENUMS.DYNAMIC_STATUS.LAUNCHING, - ENUMS.DYNAMIC_STATUS.HOOKING, - ENUMS.DYNAMIC_STATUS.READY, - ENUMS.DYNAMIC_STATUS.RUNNING, - ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN, - ].includes(status); + get isCancelled() { + return this.status === ENUMS.DYNAMIC_SCAN_STATUS.CANCELLED; } - get isNeitherNoneNorReady() { - const status = this.status; - return ![ENUMS.DYNAMIC_STATUS.READY, ENUMS.DYNAMIC_STATUS.NONE].includes( - status + get isStarting() { + return ( + this.isInqueue || + this.isBooting || + this.isInstalling || + this.isLaunching || + this.isHooking ); } - get startingScanStatus() { - return this.isDynamicStatusStarting; + get isStartingOrShuttingInProgress() { + return this.isStarting || this.isShuttingDown; } - get showScheduleAutomatedDynamicScan() { - const status = this.status; - return status !== ENUMS.DYNAMIC_STATUS.INQUEUE; + get isDynamicStatusNoneOrError() { + return this.isNone || this.isStatusError; } - get isRunning() { - const status = this.status; - return status === ENUMS.DYNAMIC_STATUS.RUNNING; + get isReadyOrRunning() { + return this.isReady || this.isRunning; + } + + get computedStatus() { + if (this.isStartingOrShuttingInProgress) { + return DsComputedStatus.IN_PROGRESS; + } + + if (this.isRunning || this.isReady) { + return DsComputedStatus.RUNNING; + } + + if (this.isCompleted) { + return DsComputedStatus.COMPLETED; + } + + if (this.isCancelled) { + return DsComputedStatus.CANCELLED; + } + + if (this.isStatusError) { + return DsComputedStatus.ERROR; + } + + return DsComputedStatus.NOT_STARTED; } get statusText() { - const tDeviceInQueue = this.intl.t('deviceInQueue'); - const tDeviceBooting = this.intl.t('deviceBooting'); - const tDeviceDownloading = this.intl.t('deviceDownloading'); - const tDeviceInstalling = this.intl.t('deviceInstalling'); - const tDeviceLaunching = this.intl.t('deviceLaunching'); - const tDeviceHooking = this.intl.t('deviceHooking'); - const tDeviceShuttingDown = this.intl.t('deviceShuttingDown'); - const tDeviceCompleted = this.intl.t('deviceCompleted'); - const tDeviceRunning = this.intl.t('inProgress'); - - switch (this.status) { - case ENUMS.DYNAMIC_STATUS.INQUEUE: - return tDeviceInQueue; - case ENUMS.DYNAMIC_STATUS.BOOTING: - return tDeviceBooting; - case ENUMS.DYNAMIC_STATUS.DOWNLOADING: - return tDeviceDownloading; - case ENUMS.DYNAMIC_STATUS.INSTALLING: - return tDeviceInstalling; - case ENUMS.DYNAMIC_STATUS.LAUNCHING: - return tDeviceLaunching; - case ENUMS.DYNAMIC_STATUS.HOOKING: - return tDeviceHooking; - case ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN: - return tDeviceShuttingDown; - case ENUMS.DYNAMIC_STATUS.COMPLETED: - return tDeviceCompleted; - case ENUMS.DYNAMIC_STATUS.RUNNING: - return tDeviceRunning; - default: - return 'Unknown Status'; + if (this.isInqueue) { + return this.intl.t('deviceInQueue'); + } + + if (this.isBooting) { + return this.intl.t('deviceBooting'); + } + + if (this.isInstalling) { + return this.intl.t('deviceInstalling'); + } + + if (this.isLaunching) { + return this.intl.t('deviceLaunching'); + } + + if (this.isHooking) { + return this.intl.t('deviceHooking'); + } + + if (this.isRunning) { + return this.intl.t('running'); + } + + if (this.isShuttingDown) { + return this.intl.t('deviceShuttingDown'); + } + + if (this.isCompleted) { + return this.intl.t('deviceCompleted'); + } + + if (this.isStatusError) { + return this.intl.t('errored'); } + + if (this.isCancelled) { + return this.intl.t('cancelled'); + } + + return this.intl.t('unknown'); } } diff --git a/app/models/file.ts b/app/models/file.ts index 2fda6d75f..1fa5d6a66 100644 --- a/app/models/file.ts +++ b/app/models/file.ts @@ -133,6 +133,12 @@ export default class FileModel extends ModelBaseMixin { @belongsTo('file', { inverse: null, async: true }) declare previousFile: AsyncBelongsTo; + async getLastDynamicScan(fileId: string, mode: number) { + const adapter = this.store.adapterFor('file'); + + return await adapter.getLastDynamicScan(fileId, mode); + } + analysesSorting = ['computedRisk:desc']; scanProgressClass(type?: boolean) { diff --git a/app/models/profile.ts b/app/models/profile.ts index 6cad5b2b9..f70ea830a 100644 --- a/app/models/profile.ts +++ b/app/models/profile.ts @@ -3,14 +3,8 @@ import Model, { AsyncHasMany, attr, hasMany } from '@ember-data/model'; import type FileModel from './file'; export enum ProfileDynamicScanMode { - MANUAL = 'Manual', - AUTOMATED = 'Automated', -} - -export enum ProfilleCapabilitiesTranslationsMap { - dsAutomatedSimRequired = 'sim', - dsAutomatedVpnRequired = 'vpn', - dsAutomatedPinLockRequired = 'pinLock', + MANUAL = 0, + AUTOMATED = 1, } interface ValueObject { @@ -37,33 +31,6 @@ export type SaveReportPreferenceData = Pick< export type SetProfileRegulatorPrefData = { value: boolean }; -export type ProfileDSManualDevicePrefData = { - id: string; - ds_manual_device_selection: number; - ds_manual_device_identifier: string; -}; - -export type SetProfileDSManualDevicePrefData = Partial< - Omit ->; - -export type ProfileDSAutomatedDevicePrefData = { - id: string; - ds_automated_device_selection: number; - ds_automated_device_type: number; - ds_automated_device_identifier: string; - ds_automated_platform_version_min: string; - ds_automated_platform_version_max: string; - ds_automated_sim_required: boolean; - ds_automated_pin_lock_required: boolean; - ds_automated_vpn_required: boolean; - ds_automated_use_reserved_device: boolean; -}; - -export type SetProfileDSAutomatedDevicePrefData = Partial< - Omit ->; - export type ProfileRegulatoryReportPreference = | 'pcidss' | 'hipaa' @@ -82,62 +49,12 @@ export default class ProfileModel extends Model { @attr declare dynamicScanMode: ProfileDynamicScanMode; - // Manual Scan - @attr('string') - declare dsManualDeviceIdentifier: string; - - @attr - declare dsManualDeviceSelection: number; - - // Automated Scan - @attr('string') - declare dsAutomatedDeviceIdentifier: string; - - @attr('string') - declare dsAutomatedPlatformVersionMin: string; - - @attr('string') - declare dsAutomatedPlatformVersionMax: string; - - @attr('boolean') - declare dsAutomatedSimRequired: boolean; - - @attr('boolean') - declare dsAutomatedPinLockRequired: boolean; - - @attr('boolean') - declare dsAutomatedVpnRequired: boolean; - - @attr('boolean') - declare dsAutomatedUseReservedDevice: boolean; - - @attr - declare dsAutomatedDeviceSelection: number; - - @attr - declare dsAutomatedDeviceType: number; - @hasMany('file', { inverse: 'profile', async: true }) declare files: AsyncHasMany; @attr declare reportPreference: ProfileReportPreference; - get profileCapabilities() { - return Object.keys(ProfilleCapabilitiesTranslationsMap).reduce( - (capabilities, key) => { - const type = key as keyof typeof ProfilleCapabilitiesTranslationsMap; - - if (this[type]) { - capabilities.push(ProfilleCapabilitiesTranslationsMap[type]); - } - - return capabilities; - }, - [] as string[] - ); - } - saveReportPreference(data: SaveReportPreferenceData) { const adapter = this.store.adapterFor(this.adapterName); @@ -158,34 +75,6 @@ export default class ProfileModel extends Model { return adapter.unsetShowPreference(this, preference); } - - getDsManualDevicePreference(): Promise { - const adapter = this.store.adapterFor(this.adapterName); - - return adapter.getDsManualDevicePreference(this); - } - - getDsAutomatedDevicePreference(): Promise { - const adapter = this.store.adapterFor(this.adapterName); - - return adapter.getDsAutomatedDevicePreference(this); - } - - setDSManualDevicePrefData( - data: SetProfileDSManualDevicePrefData - ): Promise { - const adapter = this.store.adapterFor(this.adapterName); - - return adapter.setDSManualDevicePrefData(this, data); - } - - setDSAutomatedDevicePrefData( - data: SetProfileDSAutomatedDevicePrefData - ): Promise { - const adapter = this.store.adapterFor(this.adapterName); - - return adapter.setDSAutomatedDevicePrefData(this, data); - } } declare module 'ember-data/types/registries/model' { diff --git a/app/router.ts b/app/router.ts index edf8dc126..12ccff798 100644 --- a/app/router.ts +++ b/app/router.ts @@ -203,7 +203,7 @@ Router.map(function () { this.route('dynamic-scan', function () { this.route('manual'); - // this.route('automated'); + this.route('automated'); this.route('results'); }); }); diff --git a/app/serializers/dynamicscan.js b/app/serializers/dynamicscan.js new file mode 100644 index 000000000..3d486536d --- /dev/null +++ b/app/serializers/dynamicscan.js @@ -0,0 +1,8 @@ +import DRFSerializer from './drf'; +import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest'; + +export default DRFSerializer.extend(EmbeddedRecordsMixin, { + attrs: { + deviceUsed: { embedded: 'always' }, + }, +}); diff --git a/app/services/organization.ts b/app/services/organization.ts index 3d1982074..100416316 100644 --- a/app/services/organization.ts +++ b/app/services/organization.ts @@ -1,9 +1,10 @@ import Service from '@ember/service'; import { inject as service } from '@ember/service'; -import ENV from 'irene/config/environment'; -import Store from '@ember-data/store'; -import OrganizationModel from '../models/organization'; import { tracked } from '@glimmer/tracking'; +import type Store from '@ember-data/store'; + +import ENV from 'irene/config/environment'; +import type OrganizationModel from 'irene/models/organization'; export default class OrganizationService extends Service { @service declare store: Store; @@ -31,9 +32,9 @@ export default class OrganizationService extends Service { } } - async load() { - const orgs = await this.store.findAll('organization'); - const selectedOrg = orgs.map((_) => _)[0]; + async fetchOrganization() { + const organizations = await this.store.findAll('organization'); + const selectedOrg = organizations.slice(-1)[0]; if (selectedOrg) { this.selected = selectedOrg; @@ -43,7 +44,13 @@ export default class OrganizationService extends Service { ENV.notifications ); } + } + /** + * Loads Organization and check if security dashboard is enabled + */ + async load() { + await this.fetchOrganization(); await this.setSecurityDashboardEnabled(); } } diff --git a/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs b/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs index a75480994..14702814c 100644 --- a/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs +++ b/app/templates/authenticated/dashboard/file/dynamic-scan/manual.hbs @@ -1,14 +1,6 @@ {{page-title (t 'manual')}} - - - \ No newline at end of file + @profileId={{@model.profileId}} +/> \ No newline at end of file diff --git a/mirage/factories/available-automated-device.ts b/mirage/factories/available-automated-device.ts new file mode 100644 index 000000000..49b7f9e87 --- /dev/null +++ b/mirage/factories/available-automated-device.ts @@ -0,0 +1,3 @@ +import DeviceFactory from './device'; + +export default DeviceFactory.extend({}); diff --git a/mirage/factories/available-manual-device.ts b/mirage/factories/available-manual-device.ts new file mode 100644 index 000000000..49b7f9e87 --- /dev/null +++ b/mirage/factories/available-manual-device.ts @@ -0,0 +1,3 @@ +import DeviceFactory from './device'; + +export default DeviceFactory.extend({}); diff --git a/mirage/factories/device.ts b/mirage/factories/device.ts index d90b76acd..f03581118 100644 --- a/mirage/factories/device.ts +++ b/mirage/factories/device.ts @@ -1,12 +1,31 @@ import { Factory } from 'miragejs'; import { faker } from '@faker-js/faker'; + import ENUMS from 'irene/enums'; export default Factory.extend({ - version: faker.number.int(), - isTablet: faker.datatype.boolean(), + state: () => faker.helpers.arrayElement(['available', 'busy', 'offline']), + + device_identifier: () => + faker.string.alphanumeric({ length: 7, casing: 'upper' }), - platform() { - return faker.helpers.arrayElement(ENUMS.PLATFORM.VALUES); - }, + address: () => faker.internet.ip(), + is_connected: () => faker.datatype.boolean(), + is_active: () => faker.datatype.boolean(), + is_tablet: () => faker.datatype.boolean(), + is_reserved: () => faker.datatype.boolean(), + platform: () => faker.helpers.arrayElement(ENUMS.PLATFORM.BASE_VALUES), + platform_version: () => faker.system.semver(), + cpu_architecture: () => + faker.helpers.arrayElement(['arm64', 'x86_64', 'arm']), + model: () => faker.helpers.arrayElement(['iPhone', 'iPad', 'Pixel']), + has_sim: () => faker.datatype.boolean(), + sim_network: () => faker.company.name(), + sim_phone_number: () => faker.phone.number(), + has_pin_lock: () => faker.datatype.boolean(), + has_vpn: () => faker.datatype.boolean(), + vpn_package_name: () => faker.system.commonFileName(), + has_persistent_apps: () => faker.datatype.boolean(), + persistent_apps: () => [], + has_vnc: () => faker.datatype.boolean(), }); diff --git a/mirage/factories/ds-automated-device-preference.ts b/mirage/factories/ds-automated-device-preference.ts new file mode 100644 index 000000000..a40c60416 --- /dev/null +++ b/mirage/factories/ds-automated-device-preference.ts @@ -0,0 +1,22 @@ +import { faker } from '@faker-js/faker'; +import { Factory } from 'miragejs'; + +import ENUMS from 'irene/enums'; + +export default Factory.extend({ + ds_automated_device_selection: () => + faker.helpers.arrayElement(ENUMS.DS_AUTOMATED_DEVICE_SELECTION.BASE_VALUES), + + ds_automated_device_selection_display: faker.lorem.sentence(), + + ds_automated_device_type: () => + faker.helpers.arrayElement(ENUMS.DS_AUTOMATED_DEVICE_TYPE.BASE_VALUES), + + ds_automated_platform_version_min: '', + ds_automated_platform_version_max: '', + + ds_automated_device_identifier: () => + faker.string.alphanumeric({ length: 7, casing: 'upper' }), + + ds_automated_use_reserved_device: null, +}); diff --git a/mirage/factories/ds-manual-device-preference.ts b/mirage/factories/ds-manual-device-preference.ts new file mode 100644 index 000000000..ddc247ae0 --- /dev/null +++ b/mirage/factories/ds-manual-device-preference.ts @@ -0,0 +1,14 @@ +import { faker } from '@faker-js/faker'; +import { Factory } from 'miragejs'; + +import ENUMS from 'irene/enums'; + +export default Factory.extend({ + ds_manual_device_selection: () => + faker.helpers.arrayElement(ENUMS.DS_MANUAL_DEVICE_SELECTION.BASE_VALUES), + + ds_manual_device_selection_display: faker.lorem.sentence(), + + ds_manual_device_identifier: () => + faker.string.alphanumeric({ length: 7, casing: 'upper' }), +}); diff --git a/mirage/factories/dynamicscan-mode.ts b/mirage/factories/dynamicscan-mode.ts deleted file mode 100644 index fe17bb3b5..000000000 --- a/mirage/factories/dynamicscan-mode.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { faker } from '@faker-js/faker'; - -import Base from './base'; - -export default Base.extend({ - id(i) { - return i + 1; - }, - - dynamicscan_mode: () => faker.helpers.arrayElement(['Manual', 'Automated']), -}); diff --git a/mirage/factories/dynamicscan-old.ts b/mirage/factories/dynamicscan-old.ts deleted file mode 100644 index 8dea3944e..000000000 --- a/mirage/factories/dynamicscan-old.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Factory } from 'miragejs'; -import { faker } from '@faker-js/faker'; -import ENUMS from 'irene/enums'; - -export default Factory.extend({ - api_scan: faker.datatype.boolean(), - created_on: faker.date.recent().toString(), - updated_on: faker.date.recent().toString(), - ended_on: faker.date.recent().toString(), - device_type: faker.helpers.arrayElement(ENUMS.DEVICE_TYPE.BASE_VALUES), - status: faker.helpers.arrayElement(ENUMS.DYNAMIC_STATUS.VALUES), - platform_version: faker.system.semver(), - proxy_host: faker.internet.ip(), - proxy_port: faker.internet.port(), -}); diff --git a/mirage/factories/dynamicscan.ts b/mirage/factories/dynamicscan.ts new file mode 100644 index 000000000..6c456b4de --- /dev/null +++ b/mirage/factories/dynamicscan.ts @@ -0,0 +1,28 @@ +import { Factory } from 'miragejs'; +import { faker } from '@faker-js/faker'; +import ENUMS from 'irene/enums'; + +export default Factory.extend({ + id(i: number) { + return i + 1; + }, + + file: null, + package_name: faker.internet.domainName(), + mode: faker.helpers.arrayElement(ENUMS.DYNAMIC_MODE.VALUES), + mode_display: '', + status: faker.helpers.arrayElement(ENUMS.DYNAMIC_SCAN_STATUS.VALUES), + status_display: '', + moriarty_dynamicscanrequest_id: () => faker.string.uuid(), + moriarty_dynamicscan_id: () => faker.string.uuid(), + moriarty_dynamicscan_token: () => faker.string.uuid(), + started_by_user: null, + stopped_by_user: null, + created_on: () => faker.date.recent().toISOString(), + ended_on: () => faker.date.recent().toISOString(), + auto_shutdown_on: () => faker.date.recent().toISOString(), + device_used: null, + device_preference: null, + error_code: '', + error_message: '', +}); diff --git a/mirage/models/dynamicscan-mode.ts b/mirage/models/dynamicscan.ts similarity index 100% rename from mirage/models/dynamicscan-mode.ts rename to mirage/models/dynamicscan.ts diff --git a/tests/acceptance/file-details/dynamic-scan-test.js b/tests/acceptance/file-details/dynamic-scan-test.js index e8522911b..f487ebd01 100644 --- a/tests/acceptance/file-details/dynamic-scan-test.js +++ b/tests/acceptance/file-details/dynamic-scan-test.js @@ -1,4 +1,11 @@ -import { click, currentURL, findAll, visit } from '@ember/test-helpers'; +import { + click, + currentURL, + find, + findAll, + visit, + waitUntil, +} from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupRequiredEndpoints } from '../../helpers/acceptance-utils'; @@ -7,18 +14,11 @@ import { module, test } from 'qunit'; import Service from '@ember/service'; import { t } from 'ember-intl/test-support'; import { faker } from '@faker-js/faker'; -import { selectChoose } from 'ember-power-select/test-support'; import ENUMS from 'irene/enums'; +import WebsocketService from 'irene/services/websocket'; import { analysisRiskStatus } from 'irene/helpers/analysis-risk-status'; import { objectifyEncodedReqBody } from 'irene/tests/test-utils'; -import styles from 'irene/components/ak-select/index.scss'; - -const classes = { - dropdown: styles['ak-select-dropdown'], - trigger: styles['ak-select-trigger'], - triggerError: styles['ak-select-trigger-error'], -}; class IntegrationStub extends Service { async configure(user) { @@ -34,26 +34,12 @@ class IntegrationStub extends Service { } } -class WebsocketStub extends Service { +class WebsocketStub extends WebsocketService { async connect() {} async configure() {} } -class PollServiceStub extends Service { - callback = null; - interval = null; - - startPolling(cb, interval) { - function stop() {} - - this.callback = cb; - this.interval = interval; - - return stop; - } -} - class NotificationsStub extends Service { errorMsg = null; successMsg = null; @@ -74,6 +60,51 @@ class NotificationsStub extends Service { setDefaultAutoClear() {} } +// helper function to create a dynamic scan status helper +function createDynamicScanStatusHelper(owner, dynamicscan) { + const websocket = owner.lookup('service:websocket'); + + return async function assertScanStatus( + assert, + status, + expectedStatusText, + expectedAction = null + ) { + // Update server status + dynamicscan.update({ status }); + + // Simulate websocket message + websocket.onObject({ id: dynamicscan.id, type: 'dynamicscan' }); + + // Wait for status text to update + await waitUntil(() => { + const statusEl = find('[data-test-fileDetails-dynamicScan-statusChip]'); + + return expectedStatusText + ? statusEl?.textContent.includes(expectedStatusText) + : !statusEl; + }); + + // Assert status chip text + if (expectedStatusText) { + assert + .dom('[data-test-fileDetails-dynamicScan-statusChip]') + .hasText(expectedStatusText); + } else { + assert + .dom('[data-test-fileDetails-dynamicScan-statusChip]') + .doesNotExist(); + } + + // Assert action button if specified + if (expectedAction) { + assert + .dom(`[data-test-fileDetails-dynamicScanAction="${expectedAction}"]`) + .isNotDisabled(); + } + }; +} + module('Acceptance | file-details/dynamic-scan', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); @@ -99,38 +130,28 @@ module('Acceptance | file-details/dynamic-scan', function (hooks) { const file = this.server.create('file', { id: '1', is_static_done: true, - is_dynamic_done: false, - can_run_automated_dynamicscan: true, is_active: true, project: project.id, profile: profile.id, analyses, }); - const dynamicscan = this.server.create('dynamicscan', { - id: profile.id, - mode: 1, - status: ENUMS.DYNAMIC_STATUS.RUNNING, - ended_on: null, - }); - - const dynamicscanMode = this.server.create('dynamicscan-mode', { - id: profile.id, - dynamicscan_mode: 'Automated', - }); - // service stubs this.owner.register('service:notifications', NotificationsStub); this.owner.register('service:integration', IntegrationStub); this.owner.register('service:websocket', WebsocketStub); - this.owner.register('service:poll', PollServiceStub); - this.breadcrumbsService = this.owner.lookup('service:ak-breadcrumbs'); - this.server.create('device-preference', { - id: profile.id, - }); + // lookup services + this.breadcrumbsService = this.owner.lookup('service:ak-breadcrumbs'); // server api interception + this.server.get('/v2/server_configuration', () => ({ + devicefarm_url: 'https://devicefarm.app.com', + websocket: '', + enterprise: false, + url_upload_allowed: false, + })); + this.server.get('/v2/files/:id', (schema, req) => { return schema.files.find(`${req.params.id}`)?.toJSON(); }); @@ -143,28 +164,26 @@ module('Acceptance | file-details/dynamic-scan', function (hooks) { schema.profiles.find(`${req.params.id}`)?.toJSON() ); - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); + this.server.get('/v2/files/:id/dynamicscans', (schema, req) => { + const { limit, mode } = req.queryParams || {}; + + const results = schema.dynamicscans + .where({ + file: req.params.id, + ...(mode ? { mode: Number(mode) } : {}), + }) + .models.slice(0, limit ? Number(limit) : results.length); + + return { + count: results.length, + next: null, + previous: null, + results, + }; }); - this.server.get('/profiles/:id/dynamicscan_mode', (schema, req) => { - return schema.dynamicscanModes.find(`${req.params.id}`).toJSON(); - }); - - this.server.get('/v2/dynamicscans/:id', (schema, req) => { - return schema.dynamicscans.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - - return { count: results.length, next: null, previous: null, results }; - }); - - this.server.get('v2/projects/:id/available_manual_devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - - return { count: results.length, next: null, previous: null, results }; + this.server.get('v2/profiles/:id/automation_preference', (_, req) => { + return { id: req.params.id, dynamic_scan_automation_enabled: true }; }); const store = this.owner.lookup('service:store'); @@ -175,652 +194,361 @@ module('Acceptance | file-details/dynamic-scan', function (hooks) { profile, project, store, - dynamicscan, - dynamicscanMode, }); }); - test('it visits manual DAST page', async function (assert) { - this.file = this.server.create('file', { - project: '1', - profile: '100', - dynamic_status: ENUMS.DYNAMIC_STATUS.READY, - is_dynamic_done: false, - is_active: true, - }); - - this.server.create('dynamicscan-old', { id: this.file.id }); - - this.server.get('/dynamicscan/:id', (schema, req) => { - return schema.dynamicscanOlds.find(`${req.params.id}`)?.toJSON(); - }); - - await visit(`/dashboard/file/${this.file.id}/dynamic-scan/manual`); - - // Breadcrumbs test - assert - .dom('[data-test-fileDetails-dynamicScan-header-breadcrumbContainer]') - .exists(); - - this.breadcrumbsService.breadcrumbItems.map((item) => { - assert - .dom(`[data-test-ak-breadcrumbs-auto-trail-item-key="${item.route}"]`) - .exists() - .containsText(item.title); - }); - - assert.dom('[data-test-fileDetailsSummary-root]').exists(); - - const tabs = [ - { id: 'manual-dast-tab', label: 'dastTabs.manualDAST' }, - // { id: 'automated-dast-tab', label: 'dastTabs.automatedDAST' }, - { id: 'dast-results-tab', label: 'dastTabs.dastResults' }, - ]; + test.each( + 'it renders DAST Page', + ['manual', 'automated'], + async function (assert, mode) { + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/${mode}`); - tabs.map((item) => { + // Breadcrumbs test assert - .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) + .dom('[data-test-fileDetails-dynamicScan-header-breadcrumbContainer]') .exists(); - assert - .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) - .containsText(t(item.label)); - }); - - assert - .dom(`[data-test-fileDetails-dynamicScan-manualDast-vncViewer]`) - .exists(); - - assert - .dom('[data-test-fileDetails-dynamicScan-manualDast-fullscreenBtn]') - .exists(); - - await click('[data-test-fileDetails-dynamicScan-manualDast-fullscreenBtn]'); - - assert - .dom('[data-test-vncViewer-root]') - .exists() - .hasClass(/vnc-viewer-fullscreen/); - - assert - .dom('[data-test-fileDetails-dynamicScan-manualDastFullScreen-title]') - .exists() - .containsText(t('realDevice')); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-stopBtn]') - .exists() - .containsText(t('stop')); - - assert - .dom('[data-test-fileDetails-dynamicScan-manualDastFullScreen-closeBtn]') - .exists(); - - assert.dom('[data-test-vncViewer-device]').exists(); - - await click( - '[data-test-fileDetails-dynamicScan-manualDastFullScreen-closeBtn]' - ); - - assert - .dom('[data-test-vncViewer-root]') - .exists() - .doesNotHaveClass(/vnc-viewer-fullscreen/); - - assert.dom('[data-test-NovncRfb-canvasContainer]').exists(); - }); - - // TODO: Unskip when final DAST changes are ready - // Test for Final DAST Release - test.skip('it renders dynamic scan manual', async function (assert) { - this.server.create('dynamicscan', { - id: this.profile.id, - mode: 0, - status: ENUMS.DYNAMIC_STATUS.READY, - ended_on: null, - }); + this.breadcrumbsService.breadcrumbItems.map((item) => { + assert + .dom(`[data-test-ak-breadcrumbs-auto-trail-item-key="${item.route}"]`) + .exists() + .containsText(item.title); + }); - this.server.get('/v2/dynamicscans/:id', (schema, req) => { - return schema.dynamicscans.find(`${req.params.id}`)?.toJSON(); - }); + assert.dom('[data-test-fileDetailsSummary-root]').exists(); - await visit(`/dashboard/file/${this.file.id}/dynamic-scan/manual`); + const tabs = [ + { id: 'manual-dast-tab', label: 'dastTabs.manualDAST' }, + { id: 'automated-dast-tab', label: 'dastTabs.automatedDAST' }, + { id: 'dast-results-tab', label: 'dastTabs.dastResults' }, + ]; - assert - .dom('[data-test-fileDetails-dynamicScan-header-breadcrumbContainer]') - .exists(); + tabs.map((item) => { + assert + .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) + .exists(); - const breadcrumbItems = [t('allProjects'), t('scanDetails'), t('dast')]; + assert + .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) + .containsText(t(item.label)); + }); - breadcrumbItems.map((item) => { assert - .dom( - `[data-test-fileDetails-dynamicScan-header-breadcrumbItem="${item}"]` - ) - .exists(); - }); + .dom(`[data-test-fileDetails-dynamicScan-${mode}Dast-header-text]`) + .exists() + .hasText(t('realDevice')); - assert.dom('[data-test-fileDetailsSummary-root]').exists(); + assert + .dom(`[data-test-fileDetails-dynamicScan-${mode}Dast-fullscreenBtn]`) + .doesNotExist(); - const tabs = [ - { id: 'manual-dast-tab', label: 'dastTabs.manualDAST' }, - // { id: 'automated-dast-tab', label: 'dastTabs.automatedDAST' }, - { id: 'dast-results-tab', label: 'dastTabs.dastResults' }, - ]; + assert + .dom('[data-test-fileDetails-dynamicScanAction="startBtn"]') + .exists() + .hasText( + mode === 'manual' ? t('dynamicScan') : t('dastTabs.automatedDAST') + ); - tabs.map((item) => { assert - .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) + .dom(`[data-test-fileDetails-dynamicScan-${mode}Dast-vncViewer]`) .exists(); assert - .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) - .containsText(t(item.label)); - }); - - assert - .dom(`[data-test-fileDetails-dynamicScan-manualDast-vncViewer]`) - .exists(); - - assert - .dom('[data-test-fileDetails-dynamicScan-manualDast-fullscreenBtn]') - .exists(); - - await click('[data-test-fileDetails-dynamicScan-manualDast-fullscreenBtn]'); - - assert - .dom('[data-test-fileDetails-dynamicScan-manualDast-fullscreenModal]') - .exists(); - - assert.dom(`[data-test-vncViewer-device]`).exists(); - - assert.dom('[data-test-modal-close-btn]').exists(); - - await click('[data-test-modal-close-btn]'); - - assert - .dom('[data-test-fileDetails-dynamicScan-manualDast-fullscreenModal]') - .doesNotExist(); - - assert.dom('[data-test-NovncRfb-canvasContainer]').exists(); - }); - - test('it renders expiry correctly', async function (assert) { - this.file = this.server.create('file', { - project: '1', - profile: '100', - dynamic_status: ENUMS.DYNAMIC_STATUS.READY, - is_dynamic_done: false, - is_active: true, - }); - - this.server.create('dynamicscan-old', { - id: this.file.id, - expires_on: new Date(Date.now() + 10 * 60 * 1000).toISOString(), - }); - - this.server.get('/dynamicscan/:id', (schema, req) => { - return schema.dynamicscanOlds.find(`${req.params.id}`)?.toJSON(); - }); - - await visit(`/dashboard/file/${this.file.id}/dynamic-scan/manual`); - - assert.dom('[data-test-fileDetailsSummary-root]').exists(); - - assert.dom('[data-test-fileDetails-dynamicScan-expiry]').exists(); - - assert - .dom('[data-test-fileDetails-dynamicScan-expiry-time]') - .hasText(/09:5/i); - - await click('[data-test-fileDetails-dynamicScan-expiry-extendBtn]'); - - assert - .dom('[data-test-fileDetails-dynamicScan-expiry-extendTime-menu-item]') - .exists({ count: 3 }); - - assert - .dom(`[data-test-fileDetails-dynamicScan-manualDast-vncViewer]`) - .exists(); - - assert.dom(`[data-test-vncViewer-device]`).exists(); - }); - - // TODO: For completed DAST implementation - test.skip('it renders expiry correctly', async function (assert) { - this.server.create('dynamicscan', { - id: this.profile.id, - mode: 0, - status: ENUMS.DYNAMIC_STATUS.RUNNING, - ended_on: null, - timeout_on: new Date(Date.now() + 10 * 60 * 1000).toISOString(), - }); - - this.server.get('/v2/dynamicscans/:id', (schema, req) => { - return schema.dynamicscans.find(`${req.params.id}`)?.toJSON(); - }); - - await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); - - assert.dom('[data-test-fileDetailsSummary-root]').exists(); - - assert.dom('[data-test-fileDetails-dynamicScan-expiry]').exists(); - - assert - .dom('[data-test-fileDetails-dynamicScan-expiry-time]') - .hasText(/09:5/i); - - await click('[data-test-fileDetails-dynamicScan-expiry-extendBtn]'); - - assert - .dom('[data-test-fileDetails-dynamicScan-expiry-extendTime-menu-item]') - .exists({ count: 3 }); - - assert - .dom(`[data-test-fileDetails-dynamicScan-automatedDast-vncViewer]`) - .exists(); - - assert.dom(`[data-test-vncViewer-device]`).exists(); - }); + .dom('[data-test-vncViewer-root]') + .exists() + .doesNotHaveClass(/vnc-viewer-fullscreen/); - test.skip('it renders dynamic scan automated', async function (assert) { - await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); + assert.dom('[data-test-vncViewer-device]').exists(); - assert - .dom('[data-test-fileDetails-dynamicScan-header-breadcrumbContainer]') - .exists(); - - assert.dom('[data-test-fileDetailsSummary-root]').exists(); + assert.dom('[data-test-NovncRfb-canvasContainer]').doesNotExist(); + } + ); - assert.dom('[data-test-fileDetails-dynamicScan-expiry]').exists(); + test.each( + 'it should start & stop/cancel dynamic scan', + [ + { mode: 'manual', cancelInBetween: false, expectedAssertions: 41 }, + { mode: 'manual', cancelInBetween: true, expectedAssertions: 35 }, + { mode: 'automated', cancelInBetween: false, expectedAssertions: 45 }, + { mode: 'automated', cancelInBetween: true, expectedAssertions: 38 }, + ], + async function (assert, { mode, cancelInBetween, expectedAssertions }) { + assert.expect(expectedAssertions); + + const createDynamicscan = () => + this.server.create('dynamicscan', { + file: this.file.id, + mode: ENUMS.DYNAMIC_MODE[mode.toUpperCase()], + status: ENUMS.DYNAMIC_SCAN_STATUS.NOT_STARTED, + ended_on: null, + }); - // await click('[data-test-fileDetails-dynamicScan-expiry-extendBtn]'); + const availableDevices = this.server.createList( + 'available-manual-device', + 3 + ); - // assert - // .dom('[data-test-fileDetails-dynamicScan-expiry-extendTime-menu-item]') - // .exists({ count: 3 }); + this.server.create('ds-manual-device-preference', { + id: this.profile.id, + ds_manual_device_selection: + ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE, + ds_manual_device_identifier: availableDevices[0].device_identifier, + }); - assert - .dom(`[data-test-fileDetails-dynamicScan-automatedDast-vncViewer]`) - .exists(); + this.server.create('ds-automated-device-preference', { + id: this.profile.id, + }); - assert.dom(`[data-test-vncViewer-device]`).exists(); + this.server.post('/v2/files/:id/dynamicscans', (schema, req) => { + const reqBody = objectifyEncodedReqBody(req.requestBody); - assert - .dom(`[data-test-vncViewer-automatedNote]`) - .exists() - .containsText(t('automatedScanVncNote')); + // assert request body + assert.strictEqual( + reqBody.mode, + String(ENUMS.DYNAMIC_MODE[mode.toUpperCase()]) + ); - assert - .dom('[data-test-fileDetails-dynamicScan-automatedDast-fullscreenBtn]') - .exists(); + assert.strictEqual(reqBody.enable_api_capture, 'false'); - await click( - '[data-test-fileDetails-dynamicScan-automatedDast-fullscreenBtn]' - ); + // create and set dynamicscan data reference + this.dynamicscan = createDynamicscan(); - assert - .dom('[data-test-fileDetails-dynamicScan-automatedDast-fullscreenModal]') - .exists(); + return this.dynamicscan.toJSON(); + }); - assert - .dom(`[data-test-vncViewer-automatedNote]`) - .exists() - .containsText(t('automatedScanVncNote')); + this.server.get('/v2/dynamicscans/:id', (schema, req) => { + return schema.dynamicscans.find(`${req.params.id}`)?.toJSON(); + }); - assert.dom(`[data-test-vncViewer-device]`).exists(); + this.server.delete('/v2/dynamicscans/:id', () => { + this.dynamicscan.update({ + status: ENUMS.DYNAMIC_SCAN_STATUS.STOP_SCAN_REQUESTED, + }); - assert.dom('[data-test-modal-close-btn]').exists(); + return new Response(204); + }); - await click('[data-test-modal-close-btn]'); + this.server.get('/v2/projects/:id/available_manual_devices', (schema) => { + const results = schema.availableManualDevices.all().models; - assert - .dom('[data-test-fileDetails-dynamicScan-automatedDast-fullscreenModal]') - .doesNotExist(); - }); + return { count: results.length, next: null, previous: null, results }; + }); - test.skip( - 'test: start dynamic scan', - [{ isAutomated: false }, { isAutomated: true }], - async function (assert, { isAutomated }) { - assert.expect(); + this.server.get( + '/v2/profiles/:id/ds_manual_device_preference', + (schema, req) => { + return schema.dsManualDevicePreferences + .find(`${req.params.id}`) + ?.toJSON(); + } + ); - this.file = this.store.push( - this.store.normalize('file', this.file.toJSON()) + this.server.get( + '/v2/profiles/:id/ds_automated_device_preference', + (schema, req) => { + return schema.dsAutomatedDevicePreferences + .find(`${req.params.id}`) + ?.toJSON(); + } ); - const DYNAMIC_SCAN_MODEL_ID = this.profile.id; - const scanTypeText = isAutomated ? 'Automated' : 'Manual'; + this.server.get('/profiles/:id/api_scan_options', () => ({ + id: '1', + api_url_filters: '', + })); - this.server.create('dynamicscan', { - id: DYNAMIC_SCAN_MODEL_ID, - mode: isAutomated ? 1 : 0, - status: ENUMS.DYNAMIC_STATUS.NONE, - }); + this.server.get('/profiles/:id/proxy_settings', () => ({ + id: '1', + host: faker.internet.ip(), + port: faker.internet.port(), + enabled: false, + })); - this.server.get('/profiles/:id/api_scan_options', (schema, req) => { - return { id: req.params.id, api_url_filters: '' }; - }); + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/${mode}`); - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; - }); + assert + .dom('[data-test-fileDetails-dynamicScan-header-breadcrumbContainer]') + .exists(); - this.server.get('v2/profiles/:id/ds_manual_device_preference', () => { - return { - ds_manual_device_selection: - ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, - }; - }); + assert.dom('[data-test-fileDetailsSummary-root]').exists(); - this.server.post('/v2/files/:id/dynamicscans', (_, req) => { - const reqBody = objectifyEncodedReqBody(req.requestBody); + assert + .dom(`[data-test-fileDetails-dynamicScan-${mode}Dast-header-text]`) + .hasText(t('realDevice')); - assert.strictEqual(reqBody.mode, scanTypeText); + assert + .dom('[data-test-fileDetails-dynamicScanAction="startBtn"]') + .isNotDisabled() + .hasText( + mode === 'manual' ? t('dynamicScan') : t('dastTabs.automatedDAST') + ); - assert.strictEqual(reqBody.enable_api_capture, 'false'); + assert + .dom(`[data-test-fileDetails-dynamicScan-${mode}Dast-fullscreenBtn]`) + .doesNotExist(); - // Start automated scan for dynamic scan object - this.server.db.dynamicscans.update(DYNAMIC_SCAN_MODEL_ID, { - status: isAutomated - ? ENUMS.DYNAMIC_STATUS.RUNNING - : ENUMS.DYNAMIC_STATUS.READY, - }); + assert + .dom(`[data-test-fileDetails-dynamicScan-${mode}Dast-vncViewer]`) + .exists(); - return new Response(200); - }); + assert.dom('[data-test-NovncRfb-canvasContainer]').doesNotExist(); - await visit( - `/dashboard/file/${this.file.id}/dynamic-scan/${scanTypeText.toLowerCase()}` - ); + // Click start button + await click('[data-test-fileDetails-dynamicScanAction="startBtn"]'); assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .exists() - .containsText(`${scanTypeText} DAST`); - - // Load dynamic scan drawer - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); + .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer]') + .exists(); assert .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer-title]') - .exists() - .hasText(`${scanTypeText} DAST`); - - const drawerDynamicScanStartBtn = - '[data-test-fileDetails-dynamicScanDrawer-startBtn]'; - - if (!isAutomated) { - assert.dom(drawerDynamicScanStartBtn).exists().isDisabled(); - - // Select "Any Device" - await selectChoose( - `.${classes.trigger}`, - 'Use any available device with any OS version' + .hasText( + mode === 'manual' + ? t('dastTabs.manualDAST') + : t('dastTabs.automatedDAST') ); - // Start button should be enabled - assert.dom(drawerDynamicScanStartBtn).isNotDisabled(); - - await click(drawerDynamicScanStartBtn); - } else { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-closeBtn]' - ) - .exists(); - - // Drawer CTA Buttons - assert.dom(drawerDynamicScanStartBtn).exists().hasText('Restart Scan'); + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') + .isNotDisabled() + .hasText( + mode === 'manual' + ? t('start') + : t('modalCard.dynamicScan.restartScan') + ); + if (mode === 'automated') { assert .dom( '[data-test-fileDetails-dynamicScanDrawer-settingsPageRedirectBtn]' ) - .exists() - .hasText('Go to General Settings') + .hasText(t('modalCard.dynamicScan.goToGeneralSettings')) .hasAttribute('target', '_blank') .hasAttribute( 'href', - `/dashboard/project/${this.file.project.id}/settings` + `/dashboard/project/${this.file.project}/settings` ); - - await click(drawerDynamicScanStartBtn); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-settingsPageRedirectBtn]' - ) - .doesNotExist(); } - // Modal should be closed after successful start action + // start dynamic scan + await click('[data-test-fileDetails-dynamicScanDrawer-startBtn]'); + assert .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer]') .doesNotExist(); - assert.dom(drawerDynamicScanStartBtn).doesNotExist(); - } - ); - - test.skip( - 'test: cancel/stop dynamic scan', - [{ isAutomated: false }], - async function (assert, { isAutomated }) { - assert.expect(); - - this.file = this.store.push( - this.store.normalize('file', this.file.toJSON()) + const assertScanStatus = createDynamicScanStatusHelper( + this.owner, + this.dynamicscan ); - const DYNAMIC_SCAN_MODEL_ID = this.profile.id; - const scanTypeText = isAutomated ? 'Automated' : 'Manual'; - - const dynamicScan = this.server.create('dynamicscan', { - id: DYNAMIC_SCAN_MODEL_ID, - mode: isAutomated ? 1 : 0, - status: isAutomated - ? ENUMS.DYNAMIC_STATUS.RUNNING - : ENUMS.DYNAMIC_STATUS.NONE, - }); - - this.server.get('/profiles/:id/api_scan_options', (schema, req) => { - return { id: req.params.id, api_url_filters: '' }; - }); - - this.server.delete('/dynamicscans/:id', (_, req) => { - // It deletes the correct dynamic scan ID - assert.strictEqual(dynamicScan.id, req.params.id); - - // Start automated scan for dynamic scan object - this.server.db.dynamicscans.update(DYNAMIC_SCAN_MODEL_ID, { - status: ENUMS.DYNAMIC_STATUS.NONE, - }); - - return new Response(204); - }); - - await visit( - `/dashboard/file/${this.file.id}/dynamic-scan/${scanTypeText.toLowerCase()}` + // Test the scan status flow + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.IN_QUEUE, + t('deviceInQueue'), + 'cancelBtn' ); - const statusChipSelector = - '[data-test-fileDetails-dynamicScan-statusChip]'; - const stopBtn = '[data-test-fileDetails-dynamicScanAction-stopBtn]'; - const cancelBtn = '[data-test-fileDetails-dynamicScanAction-cancelBtn]'; - const scanStartBtn = '[data-test-fileDetails-dynamicScanAction-startBtn]'; - - assert.dom(scanStartBtn).doesNotExist(); - - if (!isAutomated) { - assert.dom(statusChipSelector).doesNotExist(); - - assert.dom(stopBtn).exists().containsText('Stop'); - - await click(stopBtn); + if (mode === 'automated') { + assert.dom('[data-test-vncViewer-manualScanNote]').doesNotExist(); - assert.dom(statusChipSelector).exists().containsText('Stopping'); - - assert.dom(stopBtn).doesNotExist(); - assert.dom(scanStartBtn).doesNotExist(); - } else { - assert.dom(statusChipSelector).exists().containsText('In Progress'); - assert.dom(cancelBtn).exists().containsText('Cancel Scan'); + assert.dom('[data-test-vncViewer-scanTriggeredNote]').exists(); assert - .dom('[data-test-fileDetails-dynamicScan-expiry-time]') - .exists() - .hasText('00:00'); + .dom('[data-test-vncViewer-automatedNote]') + .hasText(`${t('note')} - ${t('automatedScanVncNote')}`); + } else { + assert.dom('[data-test-vncViewer-automatedNote]').doesNotExist(); + assert.dom('[data-test-vncViewer-scanTriggeredNote]').doesNotExist(); assert - .dom('[data-test-vncViewer-scanTriggeredAutomaticallyText]') - .exists() - .hasText('Scan triggered automatically on app upload.'); - - await click(cancelBtn); - - assert.dom(statusChipSelector).hasText('Stopping'); - assert.dom(cancelBtn).doesNotExist(); - - assert.dom(scanStartBtn).exists().containsText(`${scanTypeText} DAST`); + .dom('[data-test-vncViewer-manualScanNote]') + .hasText(`${t('note')} - ${t('dynamicScanText')}`); } - } - ); - test.skip('it should render toggle dast ui if automated dast is not enabled', async function (assert) { - this.dynamicscanMode.update({ dynamicscan_mode: 'Manual' }); + assert.dom('[data-test-fileDetails-dynamicScan-expiry]').doesNotExist(); - this.server.create('proxy-setting', { id: this.profile.id }); + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.DEVICE_ALLOCATED, + t('deviceBooting'), + 'cancelBtn' + ); - this.server.get('/profiles/:id/proxy_settings', (schema, req) => { - return schema.proxySettings.find(`${req.params.id}`)?.toJSON(); - }); + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.INSTALLING, + t('deviceInstalling'), + 'cancelBtn' + ); - this.server.get('/profiles/:id/api_scan_options', (_, req) => ({ - id: req.params.id, - api_url_filters: '', - })); + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.LAUNCHING, + t('deviceLaunching'), + 'cancelBtn' + ); - this.server.get( - '/v2/projects/:projectId/scan_parameter_groups', - function (schema) { - const data = schema.scanParameterGroups.all().models; - - return { - count: data.length, - next: null, - previous: null, - results: data, - }; - } - ); + if (cancelInBetween) { + // Cancel scan + await click('[data-test-fileDetails-dynamicScanAction="cancelBtn"]'); + } else { + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.CONFIGURING_API_CAPTURE, + t('deviceHooking'), + 'cancelBtn' + ); - this.server.get( - '/v2/scan_parameter_groups/:id/scan_parameters', - (schema) => { - const data = schema.scanParameters.all().models; - - return { - count: data.length, - next: null, - previous: null, - results: data, - }; - } - ); + await assertScanStatus( + assert, + mode === 'manual' + ? ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION + : ENUMS.DYNAMIC_SCAN_STATUS.INITIATING_AUTO_INTERACTION, + mode === 'manual' ? null : t('running'), + 'stopBtn' + ); - this.server.get( - '/organizations/:id/projects/:projectId/collaborators', - (schema) => { - const results = schema.projectCollaborators.all().models; + if (mode === 'automated') { + assert.dom('[data-test-vncViewer-scanTriggeredNote]').exists(); - return { count: results.length, next: null, previous: null, results }; - } - ); + assert + .dom('[data-test-vncViewer-automatedNote]') + .hasText(`${t('note')} - ${t('automatedScanVncNote')}`); + } else { + assert.dom('[data-test-fileDetails-dynamicScan-expiry]').exists(); + } - this.server.get( - '/organizations/:orgId/projects/:projectId/teams', - (schema) => { - const results = schema.projectTeams.all().models; + assert.dom('[data-test-vncViewer-manualScanNote]').doesNotExist(); - return { count: results.length, next: null, previous: null, results }; + // Stop scan + await click('[data-test-fileDetails-dynamicScanAction="stopBtn"]'); } - ); - - this.server.get( - '/organizations/:id/github_repos', - () => new Response(404, {}, { detail: 'Github not integrated' }) - ); - - this.server.get( - '/projects/:id/github', - () => new Response(400, {}, { detail: 'Github not integrated' }) - ); - this.server.get( - '/organizations/:id/jira_projects', - () => new Response(404, {}, { detail: 'JIRA not integrated' }) - ); - - this.server.get( - '/projects/:id/jira', - () => new Response(404, {}, { detail: 'JIRA not integrated' }) - ); - - this.server.get('/profiles/:id/dynamicscan_mode', (schema, req) => { - return schema.dynamicscanModes.find(`${req.params.id}`).toJSON(); - }); - - await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); - - assert - .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledCard]') - .exists(); - - assert - .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledTitle]') - .exists() - .containsText(t('toggleAutomatedDAST')); - - // TODO: add containsText here after correct text is given - assert - .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledDesc]') - .exists(); - - assert - .dom( - '[data-test-fileDetails-dynamicScan-automatedDast-disabledActionBtn]' - ) - .exists(); - - await click( - '[data-test-fileDetails-dynamicScan-automatedDast-disabledActionBtn]' - ); - - assert.strictEqual( - currentURL(), - `/dashboard/project/${this.file.id}/settings` - ); - }); - - test.skip('it should render upselling ui if automated dast is not enabled', async function (assert) { - this.file.update({ can_run_automated_dynamicscan: false }); + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.SHUTTING_DOWN, + t('deviceShuttingDown') + ); - this.organization.update({ - features: { - dynamicscan_automation: false, - }, - }); + assert.dom('[data-test-fileDetails-dynamicScanAction]').doesNotExist(); - await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); + await assertScanStatus( + assert, + cancelInBetween + ? ENUMS.DYNAMIC_SCAN_STATUS.CANCELLED + : ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + cancelInBetween ? t('cancelled') : t('deviceCompleted'), + cancelInBetween ? 'startBtn' : 'restartBtn' + ); - assert.dom('[data-test-automated-dast-upselling]').exists(); - }); + assert.dom('[data-test-vncViewer-scanTriggeredNote]').doesNotExist(); + assert.dom('[data-test-vncViewer-automatedNote]').doesNotExist(); + assert.dom('[data-test-vncViewer-manualScanNote]').doesNotExist(); + assert.dom('[data-test-fileDetails-dynamicScan-expiry]').doesNotExist(); + } + ); test('it should navigate properly on tab click', async function (assert) { await visit(`/dashboard/file/${this.file.id}/dynamic-scan/manual`); @@ -833,18 +561,17 @@ module('Acceptance | file-details/dynamic-scan', function (hooks) { .hasText(t('dastTabs.manualDAST')) .hasClass(/active-shadow/); - // TODO: Uncomment when full DAST feature is ready - // await click(tabLink('automated-dast-tab')); + await click(tabLink('automated-dast-tab')); - // assert - // .dom(tabLink('automated-dast-tab')) - // .hasText(t('dastTabs.automatedDAST')) - // .hasClass(/active-shadow/); + assert + .dom(tabLink('automated-dast-tab')) + .hasText(t('dastTabs.automatedDAST')) + .hasClass(/active-shadow/); - // assert.strictEqual( - // currentURL(), - // `/dashboard/file/${this.file.id}/dynamic-scan/automated` - // ); + assert.strictEqual( + currentURL(), + `/dashboard/file/${this.file.id}/dynamic-scan/automated` + ); await click(tabLink('dast-results-tab')); diff --git a/tests/integration/components/api-filter-test.js b/tests/integration/components/api-filter-test.js new file mode 100644 index 000000000..07eb8f10b --- /dev/null +++ b/tests/integration/components/api-filter-test.js @@ -0,0 +1,189 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, click, fillIn, findAll } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupIntl, t } from 'ember-intl/test-support'; +import Service from '@ember/service'; + +class NotificationStub extends Service { + successMsg = ''; + errorMsg = ''; + + error(msg) { + this.errorMsg = msg; + } + + success(msg) { + this.successMsg = msg; + } +} + +module('Integration | Component | api-filter', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks, 'en'); + + hooks.beforeEach(function () { + this.owner.register('service:notifications', NotificationStub); + + this.profile = this.server.create('profile'); + this.store = this.owner.lookup('service:store'); + }); + + test('it renders', async function (assert) { + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { api_url_filters: '', id: req.params.id }; + }); + + await render(hbs``); + + assert + .dom('[data-test-apiFilter-title]') + .hasText(t('templates.apiScanURLFilter')); + + assert + .dom('[data-test-apiFilter-description]') + .hasText(t('otherTemplates.specifyTheURL')); + + assert + .dom('[data-test-apiFilter-apiEndpointInput]') + .isNotDisabled() + .hasNoValue(); + + assert.dom('[data-test-helper-text]').hasText(t('templates.enterEndpoint')); + + assert + .dom('[data-test-apiFilter-addApiEndpointBtn]') + .isNotDisabled() + .hasText(t('templates.addNewUrlFilter')); + + assert.dom('[data-test-apiFilter-table]').doesNotExist(); + }); + + test('it handles URL validation and addition', async function (assert) { + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { api_url_filters: '', id: req.params.id }; + }); + + this.server.put('/profiles/:id/api_scan_options', (schema, req) => { + const [, value] = req.requestBody.split('='); + + return { + api_url_filters: decodeURIComponent(value), + id: this.profile.id, + }; + }); + + await render(hbs``); + + const notify = this.owner.lookup('service:notifications'); + + // Test empty input validation + await click('[data-test-apiFilter-addApiEndpointBtn]'); + + assert.strictEqual(notify.errorMsg, t('emptyURLFilter')); + + // Test invalid URL validation + await fillIn( + '[data-test-apiFilter-apiEndpointInput]', + 'https://api.example.com' + ); + + await click('[data-test-apiFilter-addApiEndpointBtn]'); + + assert.strictEqual( + notify.errorMsg, + `https://api.example.com ${t('invalidURL')}` + ); + + // Test valid URL addition + await fillIn('[data-test-apiFilter-apiEndpointInput]', 'api.example.com'); + await click('[data-test-apiFilter-addApiEndpointBtn]'); + + assert.strictEqual(notify.successMsg, t('urlUpdated')); + + assert.dom('[data-test-apiFilter-table]').exists(); + + // Add second URL + await fillIn('[data-test-apiFilter-apiEndpointInput]', 'api.example2.com'); + await click('[data-test-apiFilter-addApiEndpointBtn]'); + + // Verify table headers + const headers = findAll('[data-test-apiFilter-thead] th'); + assert.strictEqual(headers.length, 2); + + assert.dom(headers[0]).hasText(t('apiURLFilter')); + assert.dom(headers[1]).hasText(t('action')); + + // Verify table rows + const rows = findAll('[data-test-apiFilter-row]'); + assert.strictEqual(rows.length, 2); + + const firstRowCells = rows[0].querySelectorAll( + '[data-test-apiFilter-cell]' + ); + + assert.dom(firstRowCells[0]).hasText('api.example.com'); + + assert + .dom('[data-test-apiFilter-deleteBtn]', firstRowCells[1]) + .isNotDisabled(); + }); + + test('it handles URL deletion', async function (assert) { + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { api_url_filters: 'api.example.com', id: req.params.id }; + }); + + this.server.put('/profiles/:id/api_scan_options', (schema, req) => { + const [, value] = req.requestBody.split('='); + + return { + api_url_filters: decodeURIComponent(value), + id: this.profile.id, + }; + }); + + await render(hbs``); + + // Click delete button + await click('[data-test-apiFilter-deleteBtn]'); + + // Verify confirmation modal + assert + .dom(findAll('[data-test-ak-modal-header]')[0]) + .exists() + .hasText(t('confirm')); + + assert + .dom('[data-test-confirmbox-description]') + .hasText(t('confirmBox.removeURL')); + + assert + .dom('[data-test-confirmbox-confirmBtn]') + .isNotDisabled() + .hasText(t('yes')); + + // Confirm deletion + await click('[data-test-confirmbox-confirmBtn]'); + + // Verify URL was deleted + const notify = this.owner.lookup('service:notifications'); + + assert.strictEqual(notify.successMsg, t('urlUpdated')); + assert.strictEqual(findAll('[data-test-apiFilter-row]').length, 0); + }); + + test('it hides description when hideDescriptionText is true', async function (assert) { + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { api_url_filters: '', id: req.params.id }; + }); + + await render( + hbs`` + ); + + assert.dom('[data-test-apiFilter-description]').doesNotExist(); + }); +}); diff --git a/tests/integration/components/file-details/dynamic-scan/action/drawer-test.js b/tests/integration/components/file-details/dynamic-scan/action/drawer-test.js index ddb0bca0c..6b7b87297 100644 --- a/tests/integration/components/file-details/dynamic-scan/action/drawer-test.js +++ b/tests/integration/components/file-details/dynamic-scan/action/drawer-test.js @@ -31,17 +31,6 @@ const classes = { triggerError: styles['ak-select-trigger-error'], }; -// const dynamicScanStatusText = { -// [ENUMS.DYNAMIC_STATUS.INQUEUE]: t('deviceInQueue'), -// [ENUMS.DYNAMIC_STATUS.BOOTING]: t('deviceBooting'), -// [ENUMS.DYNAMIC_STATUS.DOWNLOADING]: t('deviceDownloading'), -// [ENUMS.DYNAMIC_STATUS.INSTALLING]: t('deviceInstalling'), -// [ENUMS.DYNAMIC_STATUS.LAUNCHING]: t('deviceLaunching'), -// [ENUMS.DYNAMIC_STATUS.HOOKING]: t('deviceHooking'), -// [ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN]: t('deviceShuttingDown'), -// [ENUMS.DYNAMIC_STATUS.COMPLETED]: t('deviceCompleted'), -// }; - class NotificationsStub extends Service { errorMsg = null; successMsg = null; @@ -184,447 +173,6 @@ module( this.owner.register('service:poll', PollServiceStub); }); - test('manual DAST: it renders dynamic scan modal', async function (assert) { - assert.expect(); - - this.server.get('v2/profiles/:id/ds_manual_device_preference', () => { - return { - ds_manual_device_selection: - ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, - - ds_manual_device_identifier: faker.string.alphanumeric({ - casing: 'upper', - length: 6, - }), - }; - }); - - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); - - await render(hbs` - - - - `); - - assert - .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer-title]') - .exists() - .hasText(t('dastTabs.manualDAST')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-closeBtn]' - ) - .exists(); - - // CTA Buttons - assert - .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') - .exists() - .hasText(t('start')); - - assert - .dom('[data-test-fileDetails-dynamicScanDrawer-cancelBtn]') - .exists() - .hasText(t('cancel')); - - assert - .dom('[data-test-fileDetails-dynamicScanDrawer-manualDast-header]') - .exists(); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-modalBodyWrapper]' - ) - .exists(); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-headerDeviceRequirements]' - ) - .exists() - .hasText(t('modalCard.dynamicScan.deviceRequirements')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-headerOSInfoDesc]' - ) - .exists() - .containsText(t('modalCard.dynamicScan.osVersion')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-headerOSInfoValue]' - ) - .exists() - .containsText(this.file.project.get('platformDisplay')) - .containsText(t('modalCard.dynamicScan.orAbove')) - .containsText(this.file.minOsVersion); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefHeaderDesc]' - ) - .exists() - .containsText(t('devicePreferences')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefSelect]' - ) - .exists(); - - await click(`.${classes.trigger}`); - - assert.dom(`.${classes.dropdown}`).exists(); - - // Select options for manual dast device seletion - let selectListItems = findAll('.ember-power-select-option'); - - const manualDastBaseChoiceValues = - ENUMS.DS_MANUAL_DEVICE_SELECTION.BASE_VALUES; - - assert.strictEqual( - selectListItems.length, - manualDastBaseChoiceValues.length - ); - - for (let i = 0; i < selectListItems.length; i++) { - const optionElement = selectListItems[i]; - const deviceSelection = manualDastBaseChoiceValues[i]; - - assert.strictEqual( - optionElement.textContent?.trim(), - t(dsManualDevicePref([deviceSelection])) - ); - } - - // Default selected is any device or nothing - // This means the available devices do not show up - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-root]' - ) - .doesNotExist(); - - assert.dom('[data-test-fileDetails-proxySettings-container]').exists(); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-enableAPICapture]' - ) - .exists() - .containsText(t('modalCard.dynamicScan.runApiScan')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-enableAPICaptureCheckbox]' - ) - .exists() - .isNotChecked(); - - // Sanity check for API URL filter section (Already tested) - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-apiFilter-title]' - ) - .hasText(t('templates.apiScanURLFilter')); - - assert.dom('[data-test-apiFilter-description]').doesNotExist(); - - assert - .dom('[data-test-apiFilter-apiEndpointInput]') - .isNotDisabled() - .hasNoValue(); - - assert - .dom('[data-test-apiFilter-addApiEndpointBtn]') - .isNotDisabled() - .hasText(t('templates.addNewUrlFilter')); - - const apiURLTitleTooltip = find( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-apiURLFilter-iconTooltip]' - ); - - await triggerEvent(apiURLTitleTooltip, 'mouseenter'); - - assert - .dom('[data-test-ak-tooltip-content]') - .exists() - .containsText(t('modalCard.dynamicScan.apiScanUrlFilterTooltipText')); - - await triggerEvent(apiURLTitleTooltip, 'mouseleave'); - }); - - test('manual DAST: test add & delete of api filter endpoint', async function (assert) { - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() - ); - - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - - return { count: results.length, next: null, previous: null, results }; - }); - - this.server.get('/profiles/:id/api_scan_options', (_, req) => { - return { api_url_filters: '', id: req.params.id }; - }); - - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: '', - port: '', - enabled: false, - }; - }); - - await render(hbs` - - - - `); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-apiFilter-title]' - ) - .hasText(t('templates.apiScanURLFilter')); - - assert.dom('[data-test-apiFilter-description]').doesNotExist(); - - assert - .dom('[data-test-apiFilter-apiEndpointInput]') - .isNotDisabled() - .hasNoValue(); - - assert - .dom('[data-test-apiFilter-addApiEndpointBtn]') - .isNotDisabled() - .hasText(t('templates.addNewUrlFilter')); - - assert.dom('[data-test-apiFilter-table]').doesNotExist(); - - const notify = this.owner.lookup('service:notifications'); - - // empty input - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - assert.strictEqual(notify.errorMsg, t('emptyURLFilter')); - - // invalid url - await fillIn( - '[data-test-apiFilter-apiEndpointInput]', - 'https://api.example.com' - ); - - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - assert.strictEqual( - notify.errorMsg, - `https://api.example.com ${t('invalidURL')}` - ); - - await fillIn('[data-test-apiFilter-apiEndpointInput]', 'api.example.com'); - - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - assert.strictEqual(notify.successMsg, t('urlUpdated')); - assert.dom('[data-test-apiFilter-table]').exists(); - - await fillIn( - '[data-test-apiFilter-apiEndpointInput]', - 'api.example2.com' - ); - - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - const headers = findAll('[data-test-apiFilter-thead] th'); - - assert.strictEqual(headers.length, 2); - assert.dom(headers[0]).hasText(t('apiURLFilter')); - assert.dom(headers[1]).hasText(t('action')); - - let rows = findAll('[data-test-apiFilter-row]'); - - assert.strictEqual(rows.length, 2); - - const firstRowCells = rows[0].querySelectorAll( - '[data-test-apiFilter-cell]' - ); - - assert.dom(firstRowCells[0]).hasText('api.example.com'); - - assert - .dom('[data-test-apiFilter-deleteBtn]', firstRowCells[1]) - .isNotDisabled(); - - // delete first url - await click( - firstRowCells[1].querySelector('[data-test-apiFilter-deleteBtn]') - ); - - assert - .dom(findAll('[data-test-ak-modal-header]')[0]) - .exists() - .hasText(t('confirm')); - - assert - .dom('[data-test-confirmbox-description]') - .hasText(t('confirmBox.removeURL')); - - assert - .dom('[data-test-confirmbox-confirmBtn]') - .isNotDisabled() - .hasText(t('yes')); - - await click('[data-test-confirmbox-confirmBtn]'); - - rows = findAll('[data-test-apiFilter-row]'); - - assert.strictEqual(notify.successMsg, t('urlUpdated')); - assert.strictEqual(rows.length, 1); - }); - - test('manual DAST: test enable api proxy toggle', async function (assert) { - assert.expect(16); - - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() - ); - - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - - return { count: results.length, next: null, previous: null, results }; - }); - - this.server.get('/profiles/:id/api_scan_options', (_, req) => { - return { api_url_filters: '', id: req.params.id }; - }); - - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; - }); - - this.server.put('/profiles/:id/proxy_settings', (_, req) => { - const data = JSON.parse(req.requestBody); - - assert.true(data.enabled); - - return { - id: req.params.id, - ...data, - }; - }); - - await render(hbs` - - - - `); - - const proxySetting = this.store.peekRecord( - 'proxy-setting', - this.file.profile.get('id') - ); - - assert.notOk(proxySetting.enabled); - - assert.dom('[data-test-fileDetails-proxySettings-container]').exists(); - - const proxySettingsTooltip = find( - '[data-test-fileDetails-proxySettings-helpIcon]' - ); - - await triggerEvent(proxySettingsTooltip, 'mouseenter'); - - assert - .dom('[data-test-fileDetails-proxySettings-helpTooltipContent]') - .exists() - .containsText(t('proxySettingsRouteVia')) - .containsText(proxySetting.port) - .containsText(proxySetting.host); - - await triggerEvent(proxySettingsTooltip, 'mouseleave'); - - assert - .dom('[data-test-fileDetails-proxySettings-enableApiProxyLabel]') - .exists() - .containsText(t('enable')) - .containsText(t('proxySettingsTitle')); - - const proxySettingsToggle = - '[data-test-fileDetails-proxySettings-enableApiProxyToggle] [data-test-toggle-input]'; - - assert.dom(proxySettingsToggle).isNotDisabled().isNotChecked(); - - await click(proxySettingsToggle); - - assert.dom(proxySettingsToggle).isNotDisabled().isChecked(); - - assert.true(proxySetting.enabled); - - const notify = this.owner.lookup('service:notifications'); - - assert.strictEqual( - notify.infoMsg, - `${t('proxyTurned')} ${t('on').toUpperCase()}` - ); - }); - test('manual DAST: it selects a device preference', async function (assert) { assert.expect(); @@ -669,11 +217,10 @@ module( return { count: results.length, next: null, previous: null, results }; }); - await render(hbs` - - + `); assert @@ -866,10 +413,9 @@ module( }); await render(hbs` - - + `); assert @@ -1038,10 +584,9 @@ module( }); await render(hbs` - - + `); assert @@ -1133,10 +678,9 @@ module( }); await render(hbs` - - + `); assert @@ -1279,8 +823,10 @@ module( const reqBody = objectifyEncodedReqBody(req.requestBody); assert.strictEqual( - reqBody.mode, - isAutomated ? 'Automated' : 'Manual' + parseInt(reqBody.mode), + isAutomated + ? ENUMS.DYNAMIC_MODE.AUTOMATED + : ENUMS.DYNAMIC_MODE.MANUAL ); if (enableApiCapture) { @@ -1292,11 +838,10 @@ module( return new Response(200); }); - await render(hbs` - - + `); if (!isAutomated) { diff --git a/tests/integration/components/file-details/dynamic-scan/manual-test.js b/tests/integration/components/file-details/dynamic-scan/manual-test.js index c572413b9..480be2b1e 100644 --- a/tests/integration/components/file-details/dynamic-scan/manual-test.js +++ b/tests/integration/components/file-details/dynamic-scan/manual-test.js @@ -1,19 +1,15 @@ -/* eslint-disable qunit/no-conditional-assertions */ -import { click, fillIn, find, findAll, render } from '@ember/test-helpers'; +import { click, find, render, triggerEvent } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupIntl, t } from 'ember-intl/test-support'; import { setupRenderingTest } from 'ember-qunit'; import { module, test } from 'qunit'; import Service from '@ember/service'; -import { Response } from 'miragejs'; -import { selectChoose } from 'ember-power-select/test-support'; import { faker } from '@faker-js/faker'; import ENUMS from 'irene/enums'; -import { deviceType } from 'irene/helpers/device-type'; +import { dsManualDevicePref } from 'irene/helpers/ds-manual-device-pref'; import styles from 'irene/components/ak-select/index.scss'; -import { compareInnerHTMLWithIntlTranslation } from 'irene/tests/test-utils'; const classes = { dropdown: styles['ak-select-dropdown'], @@ -21,21 +17,9 @@ const classes = { triggerError: styles['ak-select-trigger-error'], }; -const dynamicScanStatusText = () => ({ - [ENUMS.DYNAMIC_STATUS.INQUEUE]: t('deviceInQueue'), - [ENUMS.DYNAMIC_STATUS.BOOTING]: t('deviceBooting'), - [ENUMS.DYNAMIC_STATUS.DOWNLOADING]: t('deviceDownloading'), - [ENUMS.DYNAMIC_STATUS.INSTALLING]: t('deviceInstalling'), - [ENUMS.DYNAMIC_STATUS.LAUNCHING]: t('deviceLaunching'), - [ENUMS.DYNAMIC_STATUS.HOOKING]: t('deviceHooking'), - [ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN]: t('deviceShuttingDown'), - [ENUMS.DYNAMIC_STATUS.COMPLETED]: t('deviceCompleted'), -}); - class NotificationsStub extends Service { errorMsg = null; successMsg = null; - infoMsg = null; error(msg) { this.errorMsg = msg; @@ -44,25 +28,92 @@ class NotificationsStub extends Service { success(msg) { this.successMsg = msg; } - - info(msg) { - this.infoMsg = msg; - } } -class PollServiceStub extends Service { - callback = null; - interval = null; - - startPolling(cb, interval) { - function stop() {} - - this.callback = cb; - this.interval = interval; - - return stop; - } -} +const dynamicScanStatusTextList = [ + { + status: ENUMS.DYNAMIC_SCAN_STATUS.NOT_STARTED, + text: () => t('notStarted'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.PREPROCESSING, + text: () => t('deviceInQueue'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.PROCESSING_SCAN_REQUEST, + text: () => t('deviceInQueue'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.IN_QUEUE, + text: () => t('deviceInQueue'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.DEVICE_ALLOCATED, + text: () => t('deviceBooting'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.CONNECTING_TO_DEVICE, + text: () => t('deviceBooting'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.PREPARING_DEVICE, + text: () => t('deviceBooting'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.DOWNLOADING_AUTO_SCRIPT, + text: () => t('deviceBooting'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.CONFIGURING_AUTO_INTERACTION, + text: () => t('deviceBooting'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.INSTALLING, + text: () => t('deviceInstalling'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.LAUNCHING, + text: () => t('deviceLaunching'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.CONFIGURING_API_CAPTURE, + text: () => t('deviceHooking'), + }, + { status: ENUMS.DYNAMIC_SCAN_STATUS.HOOKING, text: () => t('deviceHooking') }, + { status: ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION, text: null }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.STOP_SCAN_REQUESTED, + text: () => t('deviceShuttingDown'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.SCAN_TIME_LIMIT_EXCEEDED, + text: () => t('deviceShuttingDown'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.SHUTTING_DOWN, + text: () => t('deviceShuttingDown'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.CLEANING_DEVICE, + text: () => t('deviceShuttingDown'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.RUNTIME_DETECTION_COMPLETED, + text: () => t('deviceShuttingDown'), + }, + { status: ENUMS.DYNAMIC_SCAN_STATUS.ERROR, text: () => t('errored') }, + { status: ENUMS.DYNAMIC_SCAN_STATUS.TIMED_OUT, text: () => t('errored') }, + { status: ENUMS.DYNAMIC_SCAN_STATUS.TERMINATED, text: () => t('errored') }, + { status: ENUMS.DYNAMIC_SCAN_STATUS.CANCELLED, text: () => t('cancelled') }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.ANALYZING, + text: () => t('deviceCompleted'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + text: () => t('deviceCompleted'), + }, +]; module( 'Integration | Component | file-details/dynamic-scan/manual', @@ -72,66 +123,6 @@ module( setupIntl(hooks, 'en'); hooks.beforeEach(async function () { - // Server mocks - this.server.get('/dynamicscan/:id', (schema, req) => { - return schema.dynamicscanOlds.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; - }); - - this.server.get('/v2/files/:id', (schema, req) => { - return schema.files.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() - ); - - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return { - ...schema.devicePreferences.find(`${req.params.id}`)?.toJSON(), - device_type: ENUMS.DEVICE_TYPE.NO_PREFERENCE, - }; - }); - - this.server.put('/profiles/:id/device_preference', (_, req) => { - const data = req.requestBody - .split('&') - .map((it) => it.split('=')) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); - - this.set('requestBody', data); - - return new Response(200); - }); - - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - - return { count: results.length, next: null, previous: null, results }; - }); - - this.server.get('/profiles/:id/api_scan_options', (_, req) => { - return { - api_url_filters: 'api.example.com,api.example2.com', - id: req.params.id, - }; - }); - - this.server.createList('organization', 1); - this.server.create('dynamicscan-old'); - const store = this.owner.lookup('service:store'); const profile = this.server.create('profile', { id: '100' }); @@ -139,6 +130,7 @@ module( const file = this.server.create('file', { project: '1', profile: profile.id, + is_active: true, }); const project = this.server.create('project', { @@ -146,34 +138,45 @@ module( id: '1', }); - const availableDevices = [ - ...this.server.createList('project-available-device', 5, { - is_tablet: true, - platform: 1, - }), - ...this.server.createList('project-available-device', 5, { - is_tablet: false, - platform: 0, - }), - ...this.server.createList('project-available-device', 5, { - is_tablet: false, - platform: 1, - }), - ]; + const availableDevices = this.server.createList( + 'available-manual-device', + 3 + ); - // choose a random device for preference - const randomDevice = faker.helpers.arrayElement( - availableDevices.filter((it) => it.platform === project.platform) + const devicePreference = this.server.create( + 'ds-manual-device-preference', + { + id: profile.id, + ds_manual_device_selection: + ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE, + ds_manual_device_identifier: availableDevices[0].device_identifier, + } ); - const devicePreference = this.server.create('device-preference', { - id: profile.id, - device_type: randomDevice.is_tablet - ? ENUMS.DEVICE_TYPE.TABLET_REQUIRED - : ENUMS.DEVICE_TYPE.PHONE_REQUIRED, - platform_version: randomDevice.platform_version, + // server mocks + this.server.get('/v2/files/:id/dynamicscans', (schema, req) => { + const { limit, mode } = req.queryParams || {}; + + const results = schema.dynamicscans + .where({ + file: req.params.id, + ...(mode ? { mode: Number(mode) } : {}), + }) + .models.slice(0, limit ? Number(limit) : results.length); + + return { + count: results.length, + next: null, + previous: null, + results, + }; + }); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); }); + // set component properties this.setProperties({ file: store.push(store.normalize('file', file.toJSON())), dynamicScanText: t('modalCard.dynamicScan.title'), @@ -184,167 +187,102 @@ module( store, }); - await this.owner.lookup('service:organization').load(); + // set up services this.owner.register('service:notifications', NotificationsStub); - this.owner.register('service:poll', PollServiceStub); }); test.each( 'test different states of dynamic scan status and button', - ENUMS.DYNAMIC_STATUS.VALUES, - async function (assert, status) { - if (status === ENUMS.DYNAMIC_STATUS.COMPLETED) { - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; - this.file.isDynamicDone = true; - } else { - this.file.dynamicStatus = status; - this.file.isDynamicDone = false; - } - - // make sure file is active - this.file.isActive = true; - - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); + dynamicScanStatusTextList, + async function (assert, { status, text }) { + const { id } = this.server.create('dynamicscan', { + file: this.file.id, + mode: ENUMS.DYNAMIC_MODE.MANUAL, + status, + ended_on: null, }); await render(hbs` - - - + `); - if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.ERROR) { - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip]') - .exists() - .hasText(t('errored')); + const dynamicscan = this.store.peekRecord('dynamicscan', id); - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .exists() - .hasText(this.dynamicScanText) - .isNotDisabled(); - } else if ( - (this.file.isDynamicStatusReady || - this.file.isDynamicStatusInProgress) && - this.file.dynamicStatus !== ENUMS.DYNAMIC_STATUS.READY - ) { - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip]') - .exists() - .hasText(this.file.statusText); + assert.strictEqual( + dynamicscan.statusText, + text && status !== ENUMS.DYNAMIC_SCAN_STATUS.NOT_STARTED + ? text() + : t('unknown') + ); - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .doesNotExist(); - } else if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.READY) { + if (text?.()) { assert .dom('[data-test-fileDetails-dynamicScan-statusChip]') - .doesNotExist(); - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .doesNotExist(); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-stopBtn]') - .exists() - .hasText(t('stop')); - } else if ( - this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.NONE && - this.file.isDynamicDone - ) { + .hasText(text()); + } else { assert .dom('[data-test-fileDetails-dynamicScan-statusChip]') - .exists() - .hasText(t('completed')); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .isNotDisabled(); - } else if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.NONE) { - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .hasText(this.dynamicScanText); + .doesNotExist(); + } + if (dynamicscan.isShuttingDown) { assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') + .dom(`[data-test-fileDetails-dynamicScanAction]`) .doesNotExist(); } else { - assert.strictEqual( - this.file.statusText, - dynamicScanStatusText()[this.file.dynamicStatus] || 'Unknown Status' - ); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .isNotDisabled() - .hasText(this.file.statusText); - - if ( - status === ENUMS.DYNAMIC_STATUS.INQUEUE && - this.file.canRunAutomatedDynamicscan - ) { + if (dynamicscan.isStarting) { assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .isNotDisabled(); + .dom(`[data-test-fileDetails-dynamicScanAction="cancelBtn"]`) + .isNotDisabled() + .hasText(t('cancelScan')); + } else if (dynamicscan.isReadyOrRunning) { + assert + .dom(`[data-test-fileDetails-dynamicScanAction="stopBtn"]`) + .isNotDisabled() + .hasText(t('stop')); + } else if (dynamicscan.isCompleted || dynamicscan.isStatusError) { + assert + .dom(`[data-test-fileDetails-dynamicScanAction="restartBtn"]`) + .isNotDisabled() + .hasText(this.dynamicScanText); + } else { + assert + .dom(`[data-test-fileDetails-dynamicScanAction="startBtn"]`) + .isNotDisabled() + .hasText(this.dynamicScanText); } } } ); test.each( - 'it should render dynamic scan modal', + 'it renders manual dynamic scan action drawer', [ - { withApiProxySetting: true }, - { withApiScan: true }, - { withAutomatedDynamicScan: true }, + { withApiProxy: false, assertions: 25 }, + { withApiProxy: true, assertions: 33 }, ], - async function ( - assert, - { withApiProxySetting, withApiScan, withAutomatedDynamicScan } - ) { - assert.expect(); - - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; - this.file.isDynamicDone = false; - this.file.isActive = true; + async function (assert, { withApiProxy, assertions }) { + assert.expect(assertions); - // make sure all available devices are not filtered based on minOsVersion - this.file.minOsVersion = this.devicePreference.platform_version; - - if (ENUMS.PLATFORM.IOS === this.project.platform) { - this.file.supportedCpuArchitectures = 'arm64'; - this.file.supportedDeviceTypes = 'iPhone, iPad'; - } - - if (withAutomatedDynamicScan) { - this.file.canRunAutomatedDynamicscan = true; - } - - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); + this.devicePreference.update({ + ds_manual_device_selection: + ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, + ds_manual_device_identifier: '', }); + this.server.get( + '/v2/profiles/:id/ds_manual_device_preference', + (schema, req) => { + return schema.dsManualDevicePreferences + .find(`${req.params.id}`) + ?.toJSON(); + } + ); + this.server.get('/profiles/:id', (schema, req) => schema.profiles.find(`${req.params.id}`)?.toJSON() ); - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - - return { count: results.length, next: null, previous: null, results }; - }); - this.server.get('/profiles/:id/api_scan_options', (_, req) => { return { api_url_filters: '', id: req.params.id }; }); @@ -352,1369 +290,167 @@ module( this.server.get('/profiles/:id/proxy_settings', (_, req) => { return { id: req.params.id, - host: withApiProxySetting ? faker.internet.ip() : '', - port: withApiProxySetting ? faker.internet.port() : '', + host: withApiProxy ? faker.internet.ip() : '', + port: withApiProxy ? faker.internet.port() : '', enabled: false, }; }); await render(hbs` - - - + `); - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .hasText(this.dynamicScanText); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .doesNotExist(); - - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); - - assert - .dom('[data-test-ak-appbar]') - .hasText(t('modalCard.dynamicScan.title')); - - assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-warningAlert]') - .hasText(t('modalCard.dynamicScan.warning')); - - if (this.file.minOsVersion) { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-deviceRequirementContainer]' - ) - .exists(); - - const deviceRequirements = [ - { - type: t('modalCard.dynamicScan.osVersion'), - value: `${this.file.project.get('platformDisplay')} ${ - this.file.minOsVersion - } ${t('modalCard.dynamicScan.orAbove')}`, - }, - this.file.supportedCpuArchitectures && { - type: t('modalCard.dynamicScan.processorArchitecture'), - value: this.file.supportedCpuArchitectures, - }, - this.file.supportedDeviceTypes && { - type: t('modalCard.dynamicScan.deviceTypes'), - value: this.file.supportedDeviceTypes, - }, - ].filter(Boolean); - - deviceRequirements.forEach(({ type, value }) => { - const container = find( - `[data-test-fileDetails-dynamicScanDrawerOld-deviceRequirementGroup="${type}"]` - ); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-deviceRequirementType]', - container - ) - .hasText(type); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-deviceRequirementValue]', - container - ) - .hasText(value); - }); - } else { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-deviceRequirementContainer]' - ) - .doesNotExist(); - } + // open the drawer + await click('[data-test-fileDetails-dynamicScanAction="startBtn"]'); assert - .dom('[data-test-projectPreference-title]') - .hasText(t('devicePreferences')); + .dom( + '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-title]' + ) + .hasText(t('dastTabs.manualDAST')); assert - .dom('[data-test-projectPreference-description]') - .hasText(t('otherTemplates.selectPreferredDevice')); + .dom( + '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-closeBtn]' + ) + .isNotDisabled(); assert .dom( - '[data-test-projectPreference-deviceTypeSelect] [data-test-form-label]' + '[data-test-fileDetails-dynamicScanDrawer-manualDast-headerDeviceRequirements]' ) - .hasText(t('deviceType')); + .hasText(t('modalCard.dynamicScan.deviceRequirements')); assert .dom( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` + '[data-test-fileDetails-dynamicScanDrawer-manualDast-headerOSInfoDesc]' ) - .hasText(t(deviceType([this.devicePreference.device_type]))); + .hasText(t('modalCard.dynamicScan.osVersion')); assert .dom( - '[data-test-projectPreference-osVersionSelect] [data-test-form-label]' + '[data-test-fileDetails-dynamicScanDrawer-manualDast-headerOSInfoValue]' ) - .hasText(t('osVersion')); + .containsText(this.file.project.get('platformDisplay')) + .containsText(t('modalCard.dynamicScan.orAbove')) + .containsText(this.file.minOsVersion); assert .dom( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefHeaderDesc]' ) - .hasText(this.devicePreference.platform_version); + .hasText(t('devicePreferences')); assert .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-runApiScanFormControl] [data-test-ak-form-label]' + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefSelect]' ) - .hasText(t('modalCard.dynamicScan.runApiScan')); + .exists(); assert .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-runApiScanFormControl] [data-test-fileDetails-dynamicScanDrawerOld-runApiScanCheckbox]' + `[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefSelect] .${classes.trigger}` ) - .isNotDisabled() - .isNotChecked(); + .hasText( + t(dsManualDevicePref([ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE])) + ); - if (withApiScan) { - await click( - '[data-test-fileDetails-dynamicScanDrawerOld-runApiScanFormControl] [data-test-fileDetails-dynamicScanDrawerOld-runApiScanCheckbox]' + if (withApiProxy) { + const proxySetting = this.store.peekRecord( + 'proxy-setting', + this.file.profile.get('id') ); - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-runApiScanFormControl] [data-test-fileDetails-dynamicScanDrawerOld-runApiScanCheckbox]' - ) - .isNotDisabled() - .isChecked(); + assert.notOk(proxySetting.enabled); assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-apiSettingsContainer]' - ) + .dom('[data-test-fileDetails-proxySettings-container]') .exists(); - compareInnerHTMLWithIntlTranslation(assert, { - selector: - '[data-test-fileDetails-dynamicScanDrawerOld-apiSettingScanDescription]', - message: t('modalCard.dynamicScan.apiScanDescription'), - }); - - assert - .dom('[data-test-apiFilter-title]') - .hasText(t('templates.apiScanURLFilter')); - - assert - .dom('[data-test-apiFilter-description]') - .hasText(t('otherTemplates.specifyTheURL')); - - assert - .dom('[data-test-apiFilter-apiEndpointInput]') - .isNotDisabled() - .hasNoValue(); - - assert - .dom('[data-test-apiFilter-addApiEndpointBtn]') - .isNotDisabled() - .hasText(t('templates.addNewUrlFilter')); - - assert.dom('[data-test-apiFilter-table]').doesNotExist(); - } else { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-apiSettingsContainer]' - ) - .doesNotExist(); - } - - const proxySetting = this.store.peekRecord( - 'proxy-setting', - this.file.profile.get('id') - ); - - if (proxySetting.hasProxyUrl) { - assert.dom('[data-test-proxySettingsView-container]').exists(); - assert - .dom( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' - ) - .isNotDisabled() - .isNotChecked(); - - assert - .dom('[data-test-proxySettingsView-enableApiProxyLabel]') + .dom('[data-test-fileDetails-proxySettings-enableApiProxyLabel]') .hasText(`${t('enable')} ${t('proxySettingsTitle')}`); - assert - .dom('[data-test-proxySettingsView-editSettings]') - .hasTagName('a') - .hasAttribute('href', '/dashboard/project/1/settings') - .hasText(t('edit')); - - assert - .dom('[data-test-proxySettingsView-proxySettingRoute]') - .hasText( - `${t('proxySettingsRouteVia')} ${proxySetting.host}:${proxySetting.port}` - ); - } else { - assert.dom('[data-test-proxySettingsView-container]').doesNotExist(); - } - - if (this.file.canRunAutomatedDynamicscan) { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-device-settings-warning]' - ) - .doesNotExist(); + const proxySettingsToggle = + '[data-test-fileDetails-proxySettings-enableApiProxyToggle] [data-test-toggle-input]'; - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanContainer]' - ) - .exists(); + assert.dom(proxySettingsToggle).isNotDisabled().isNotChecked(); - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanTitle]' - ) - .hasText(t('dynamicScanAutomation')); + const proxySettingsTooltip = find( + '[data-test-fileDetails-proxySettings-helpIcon]' + ); - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanChip]' - ) - .hasText(t('experimentalFeature')); + await triggerEvent(proxySettingsTooltip, 'mouseenter'); assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanDescription]' - ) - .hasText(t('scheduleDynamicscanDesc')); + .dom('[data-test-fileDetails-proxySettings-helpTooltipContent]') + .exists() + .containsText(t('proxySettingsRouteVia')) + .containsText(proxySetting.port) + .containsText(proxySetting.host); - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanScheduleBtn]' - ) - .isNotDisabled() - .hasText(t('scheduleDynamicscan')); + await triggerEvent(proxySettingsTooltip, 'mouseleave'); } else { assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-device-settings-warning]' - ) - .hasText( - `${t('note')}: ${t('modalCard.dynamicScan.deviceSettingsWarning')}` - ); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanContainer]' - ) + .dom('[data-test-fileDetails-proxySettings-container]') .doesNotExist(); } assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-cancelBtn]') - .isNotDisabled() - .hasText(t('cancel')); + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-enableAPICapture]' + ) + .hasText(t('modalCard.dynamicScan.runApiScan')); assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]') - .isNotDisabled() - .hasText(t('modalCard.dynamicScan.start')); - } - ); - - test('test add & delete of api filter endpoint', async function (assert) { - assert.expect(29); - - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; - this.file.isDynamicDone = false; - this.file.isActive = true; - - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() - ); - - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - - return { count: results.length, next: null, previous: null, results }; - }); - - this.server.get('/profiles/:id/api_scan_options', (_, req) => { - return { api_url_filters: '', id: req.params.id }; - }); - - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: '', - port: '', - enabled: false, - }; - }); - - await render(hbs` - - - - `); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .hasText(this.dynamicScanText); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .doesNotExist(); - - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); - - await click( - '[data-test-fileDetails-dynamicScanDrawerOld-runApiScanFormControl] [data-test-fileDetails-dynamicScanDrawerOld-runApiScanCheckbox]' - ); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-runApiScanFormControl] [data-test-fileDetails-dynamicScanDrawerOld-runApiScanCheckbox]' - ) - .isNotDisabled() - .isChecked(); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-apiSettingsContainer]' - ) - .exists(); - - compareInnerHTMLWithIntlTranslation(assert, { - selector: - '[data-test-fileDetails-dynamicScanDrawerOld-apiSettingScanDescription]', - message: t('modalCard.dynamicScan.apiScanDescription'), - }); - - assert - .dom('[data-test-apiFilter-title]') - .hasText(t('templates.apiScanURLFilter')); - - assert - .dom('[data-test-apiFilter-description]') - .hasText(t('otherTemplates.specifyTheURL')); - - assert - .dom('[data-test-apiFilter-apiEndpointInput]') - .isNotDisabled() - .hasNoValue(); - - assert - .dom('[data-test-apiFilter-addApiEndpointBtn]') - .isNotDisabled() - .hasText(t('templates.addNewUrlFilter')); - - assert.dom('[data-test-apiFilter-table]').doesNotExist(); - - const notify = this.owner.lookup('service:notifications'); - - // empty input - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - assert.strictEqual(notify.errorMsg, t('emptyURLFilter')); - - // invalid url - await fillIn( - '[data-test-apiFilter-apiEndpointInput]', - 'https://api.example.com' - ); - - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - assert.strictEqual( - notify.errorMsg, - `https://api.example.com ${t('invalidURL')}` - ); - - await fillIn('[data-test-apiFilter-apiEndpointInput]', 'api.example.com'); - - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - assert.strictEqual(notify.successMsg, t('urlUpdated')); - assert.dom('[data-test-apiFilter-table]').exists(); - - await fillIn( - '[data-test-apiFilter-apiEndpointInput]', - 'api.example2.com' - ); - - await click('[data-test-apiFilter-addApiEndpointBtn]'); - - const headers = findAll('[data-test-apiFilter-thead] th'); - - assert.strictEqual(headers.length, 2); - assert.dom(headers[0]).hasText(t('apiURLFilter')); - assert.dom(headers[1]).hasText(t('action')); - - let rows = findAll('[data-test-apiFilter-row]'); - - assert.strictEqual(rows.length, 2); - - const firstRowCells = rows[0].querySelectorAll( - '[data-test-apiFilter-cell]' - ); - - assert.dom(firstRowCells[0]).hasText('api.example.com'); - - assert - .dom('[data-test-apiFilter-deleteBtn]', firstRowCells[1]) - .isNotDisabled(); - - // delete first url - await click( - firstRowCells[1].querySelector('[data-test-apiFilter-deleteBtn]') - ); + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-enableAPICaptureCheckbox]' + ) + .isNotChecked(); - // confirm box is 2nd modal - assert.dom(findAll('[data-test-ak-appbar]')[1]).hasText(t('confirm')); + // Sanity check for API URL filter section (Already tested) + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-apiFilter-title]' + ) + .hasText(t('templates.apiScanURLFilter')); - assert - .dom('[data-test-confirmbox-description]') - .hasText(t('confirmBox.removeURL')); + assert.dom('[data-test-apiFilter-description]').doesNotExist(); - assert - .dom('[data-test-confirmbox-confirmBtn]') - .isNotDisabled() - .hasText(t('yes')); + assert + .dom('[data-test-apiFilter-apiEndpointInput]') + .isNotDisabled() + .hasNoValue(); - await click('[data-test-confirmbox-confirmBtn]'); + assert + .dom('[data-test-apiFilter-addApiEndpointBtn]') + .isNotDisabled() + .hasText(t('templates.addNewUrlFilter')); - rows = findAll('[data-test-apiFilter-row]'); + const apiURLTitleTooltip = find( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-apiURLFilter-iconTooltip]' + ); - assert.strictEqual(notify.successMsg, t('urlUpdated')); - assert.strictEqual(rows.length, 1); - }); + await triggerEvent(apiURLTitleTooltip, 'mouseenter'); - test('test enable api proxy toggle', async function (assert) { - assert.expect(10); + assert + .dom('[data-test-ak-tooltip-content]') + .exists() + .containsText(t('modalCard.dynamicScan.apiScanUrlFilterTooltipText')); - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; - this.file.isDynamicDone = false; - this.file.isActive = true; + await triggerEvent(apiURLTitleTooltip, 'mouseleave'); - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() - ); - - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - - return { count: results.length, next: null, previous: null, results }; - }); - - this.server.put('/profiles/:id/proxy_settings', (_, req) => { - const data = JSON.parse(req.requestBody); - - assert.true(data.enabled); - - return { - id: req.params.id, - ...data, - }; - }); - - await render(hbs` - - - - `); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .hasText(this.dynamicScanText); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .doesNotExist(); - - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); - - const proxySetting = this.store.peekRecord( - 'proxy-setting', - this.file.profile.get('id') - ); - - assert.dom('[data-test-proxySettingsView-container]').exists(); - - assert - .dom( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' - ) - .isNotDisabled() - .isNotChecked(); - - await click( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' - ); - - assert - .dom( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' - ) - .isNotDisabled() - .isChecked(); - - assert.true(proxySetting.enabled); - - const notify = this.owner.lookup('service:notifications'); - - assert.strictEqual( - notify.infoMsg, - `${t('proxyTurned')} ${t('on').toUpperCase()}` - ); - }); - - test.each( - 'test start dynamic scan', - [{ automatedScan: false }, { automatedScan: true }], - async function (assert, { automatedScan }) { - const isIOS = ENUMS.PLATFORM.IOS === this.project.platform; - - const file = this.server.create('file', { - project: this.project.id, - profile: '100', - dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, - is_dynamic_done: false, - can_run_automated_dynamicscan: automatedScan, - is_active: true, - min_os_version: '0', // so that we can select any option for version - supported_cpu_architectures: isIOS ? 'arm64' : '', - supported_device_types: isIOS ? 'iPhone, iPad' : '', // required for Ios to show device types - }); - - // update project with latest file - this.project.update({ - last_file_id: file.id, - }); - - // set the file - this.set( - 'file', - this.store.push(this.store.normalize('file', file.toJSON())) - ); - - // server mocks - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; - }); - - this.server.put('/dynamicscan/:id', (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.BOOTING, - }); - - return new Response(200); - }); - - this.server.post( - '/dynamicscan/:id/schedule_automation', - (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.INQUEUE, - }); - - return new Response(201); - } - ); - - await render(hbs` - - - - `); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .hasText(this.dynamicScanText); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .doesNotExist(); - - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); - - // choose device type and os version - assert - .dom( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` - ) - .hasText(t(deviceType([ENUMS.DEVICE_TYPE.NO_PREFERENCE]))); + // CTA Buttons + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') + .exists() + .hasText(t('start')); assert - .dom( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` - ) - .hasText(`${this.devicePreference.platform_version}`); - - await selectChoose( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}`, - t(deviceType([ENUMS.DEVICE_TYPE.PHONE_REQUIRED])) - ); - - // verify ui - assert - .dom( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` - ) - .hasText(t(deviceType([ENUMS.DEVICE_TYPE.PHONE_REQUIRED]))); - - // verify network data - assert.strictEqual( - this.requestBody.device_type, - `${ENUMS.DEVICE_TYPE.PHONE_REQUIRED}` - ); - - const filteredDevices = this.availableDevices.filter( - (it) => it.platform === this.project.platform && !it.is_tablet - ); - - await selectChoose( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}`, - filteredDevices[1].platform_version - ); - - // verify ui - assert - .dom( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` - ) - .hasText(`${filteredDevices[1].platform_version}`); - - // verify network data - assert.strictEqual( - this.requestBody.platform_version, - `${filteredDevices[1].platform_version}` - ); - - // enable api catpure - await click( - '[data-test-fileDetails-dynamicScanDrawerOld-runApiScanFormControl] [data-test-fileDetails-dynamicScanDrawerOld-runApiScanCheckbox]' - ); - - // verify api-filter render - let apiFilterRows = findAll('[data-test-apiFilter-row]'); - - assert.strictEqual(apiFilterRows.length, 2); - - assert - .dom( - apiFilterRows[0].querySelectorAll('[data-test-apiFilter-cell]')[0] - ) - .hasText('api.example.com'); - - assert - .dom( - apiFilterRows[1].querySelectorAll('[data-test-apiFilter-cell]')[0] - ) - .hasText('api.example2.com'); - - if (automatedScan) { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanScheduleBtn]' - ) - .isNotDisabled() - .hasText(t('scheduleDynamicscan')); - - await click( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanScheduleBtn]' - ); - } else { - assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]') - .isNotDisabled() - .hasText(t('modalCard.dynamicScan.start')); - - await click('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]'); - } - - const notify = this.owner.lookup('service:notifications'); - const poll = this.owner.lookup('service:poll'); - - assert.strictEqual( - notify.successMsg, - automatedScan ? t('scheduleDynamicscanSuccess') : t('startingScan') - ); - - // simulate polling - if (poll.callback) { - await poll.callback(); - } - - assert.strictEqual( - this.file.dynamicStatus, - automatedScan - ? ENUMS.DYNAMIC_STATUS.INQUEUE - : ENUMS.DYNAMIC_STATUS.BOOTING - ); - - // modal should close - assert.dom('[data-test-ak-appbar]').doesNotExist(); - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .doesNotExist(); - - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip]') + .dom('[data-test-fileDetails-dynamicScanDrawer-cancelBtn]') .exists() - .hasText( - dynamicScanStatusText()[ - automatedScan - ? ENUMS.DYNAMIC_STATUS.INQUEUE - : ENUMS.DYNAMIC_STATUS.BOOTING - ] - ); - } - ); - - test('test stop dynamic scan', async function (assert) { - const file = this.server.create('file', { - project: '1', - profile: '100', - dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, - is_dynamic_done: false, - is_active: true, - }); - - this.server.create('dynamicscan-old', { id: file.id }); - - this.set( - 'file', - this.store.push(this.store.normalize('file', file.toJSON())) - ); - - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; - }); - - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return { device_type: 1, id: req.params.id, platform_version: '0' }; - }); - - this.server.put('/dynamicscan/:id', (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.READY, - }); - - return new Response(200); - }); - - this.server.post( - '/dynamicscan/:id/schedule_automation', - (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.INQUEUE, - }); - - return new Response(201); - } - ); - - this.server.delete('/dynamicscan/:id', (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, - is_dynamic_done: true, - }); - - return new Response(204); - }); - - await render(hbs` - - - - `); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .hasText(this.dynamicScanText); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .doesNotExist(); - - // Open dynamic scan drawer - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); - - // Start dynamic scan - await click('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]'); - - const poll = this.owner.lookup('service:poll'); - - // simulate polling - if (poll.callback) { - await poll.callback(); - } - - assert - .dom('[data-test-fileDetails-dynamicScanAction-stopBtn]') - .hasText(t('stop')); - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .doesNotExist(); - - await click('[data-test-fileDetails-dynamicScanAction-stopBtn]'); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .doesNotExist(); - - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip]') - .exists() - .hasText(dynamicScanStatusText()[ENUMS.DYNAMIC_STATUS.SHUTTING_DOWN]); - - // simulate polling - if (poll.callback) { - await poll.callback(); - } - - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip]') - .hasText(t('completed')); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .exists() - .hasText(this.dynamicScanText); - }); - - test.each( - 'test device unavailability scenarios', - [ - { - scenario: 'preferred device not available', - removePreferredOnly: true, - expectedError: () => - t('modalCard.dynamicScan.preferredDeviceNotAvailable'), - }, - { - scenario: 'all devices allocated', - removePreferredOnly: false, - expectedError: () => - t('modalCard.dynamicScan.allDevicesAreAllocated'), - }, - { - scenario: 'minimum OS version is unsupported (Android)', - removePreferredOnly: false, - minAndroidOsVersion: 15, // Current min supported android OS is 14 - expectedError: () => - t('modalCard.dynamicScan.minOSVersionUnsupported'), - }, - { - scenario: 'minimum OS version is unsupported (iOS)', - removePreferredOnly: false, - minIOSOSVersion: 18, // Current min supported android OS is 17 - expectedError: () => - t('modalCard.dynamicScan.minOSVersionUnsupported'), - }, - ], - async function ( - assert, - { - removePreferredOnly, - expectedError, - minAndroidOsVersion, - minIOSOSVersion, - } - ) { - const preferredDeviceType = this.devicePreference.device_type; - const preferredPlatformVersion = this.devicePreference.platform_version; - - if (removePreferredOnly) { - // Remove only preferred devices - const preferredDeviceList = - this.server.db.projectAvailableDevices.where( - (ad) => - ad.platform_version === preferredPlatformVersion && - (ad.is_tablet - ? preferredDeviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED - : preferredDeviceType === ENUMS.DEVICE_TYPE.PHONE_REQUIRED) - ); - - preferredDeviceList.forEach(({ id }) => { - this.server.db.projectAvailableDevices.remove(id); - }); - } else { - // Remove all devices - this.server.db.projectAvailableDevices.remove(); - } - - const file = this.server.create('file', { - project: '1', - profile: '100', - dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, - is_dynamic_done: false, - can_run_automated_dynamicscan: false, - is_active: true, - min_os_version: minAndroidOsVersion - ? minAndroidOsVersion - : minIOSOSVersion - ? minIOSOSVersion - : faker.number.int({ min: 9, max: 12 }), - }); - - this.set( - 'file', - this.store.push(this.store.normalize('file', file.toJSON())) - ); - - // Server mocks - this.server.get('/v2/projects/:id', (schema, req) => { - const project = schema.projects.find(`${req.params.id}`)?.toJSON(); - - return { - ...project, - platform: minIOSOSVersion - ? ENUMS.PLATFORM.IOS - : minAndroidOsVersion - ? ENUMS.PLATFORM.ANDROID - : project.platform, - }; - }); - - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() - ); - - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return schema.devicePreferences.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - return { count: results.length, next: null, previous: null, results }; - }); - - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: '', - port: '', - enabled: false, - }; - }); - - await render(hbs` - - - - `); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .hasText(this.dynamicScanText); - - assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]') - .doesNotExist(); - - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); - - // Verify error states - assert - .dom( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` - ) - .hasClass(classes.triggerError); - - assert - .dom( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` - ) - .hasClass(classes.triggerError); - - assert - .dom('[data-test-projectPreference-deviceUnavailableError]') - .hasText(expectedError()); - - assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]') - .isDisabled(); - } - ); - - test('test os version filtering based on min os version', async function (assert) { - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; - this.file.isDynamicDone = false; - this.file.isActive = true; - this.file.minOsVersion = '14.0'; // Update file's min_os_version - - // Clear existing devices and create new ones with specific versions - this.server.db.projectAvailableDevices.remove(); - - this.server.createList('project-available-device', 2, { - platform: this.project.platform, - platform_version: '13.0 (d10)', - is_tablet: false, - }); - - this.server.createList('project-available-device', 2, { - platform: this.project.platform, - platform_version: '14.2', - is_tablet: false, - }); - - this.server.createList('project-available-device', 2, { - platform: this.project.platform, - platform_version: '15.0 (d101) sim', - is_tablet: false, - }); - - await render(hbs` - - - - `); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .hasText(this.dynamicScanText); - - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); - - // Open OS version dropdown - await click( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` - ); - - const options = findAll(`.${classes.dropdown} [data-option-index]`); - const versions = Array.from(options).map((el) => el.textContent.trim()); - - // Should only include "Any Version" (0) and versions >= 14.0 - assert.deepEqual( - versions, - [t('anyVersion'), '15.0 (d101) sim', '14.2'], - 'Only shows versions >= min_os_version and "Any Version" option' - ); - - // Versions below min_os_version should not be present - assert.notOk( - versions.includes('13.0 (d10)'), - 'Does not show versions below min_os_version' - ); - }); - - test.each( - 'test device type filtering based on platform and supported types', - [ - { - platform: ENUMS.PLATFORM.IOS, - supportedDeviceTypes: 'iPhone, iPad', - expectedTypes: ['No Preference', 'Phone Required', 'Tablet Required'], - }, - { - platform: ENUMS.PLATFORM.IOS, - supportedDeviceTypes: 'iPhone', - expectedTypes: ['No Preference', 'Phone Required'], - }, - { - platform: ENUMS.PLATFORM.IOS, - supportedDeviceTypes: 'iPad', - expectedTypes: ['No Preference', 'Tablet Required'], - }, - { - platform: ENUMS.PLATFORM.ANDROID, - supportedDeviceTypes: '', - expectedTypes: ['No Preference', 'Phone Required'], - }, - ], - async function ( - assert, - { platform, supportedDeviceTypes, expectedTypes } - ) { - // Update project platform - this.project.update({ platform }); - - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; - this.file.isDynamicDone = false; - this.file.isActive = true; - // Update file's supported device types - this.file.supportedDeviceTypes = supportedDeviceTypes; - - await render(hbs` - - - - `); - - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); - - // Open device type dropdown - await click( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` - ); - - const options = findAll(`.${classes.dropdown} [data-option-index]`); - - const deviceTypes = Array.from(options).map((el) => - el.textContent.trim() - ); - - assert.deepEqual( - deviceTypes, - expectedTypes.map((type) => - t( - deviceType([ - ENUMS.DEVICE_TYPE[type.replace(' ', '_').toUpperCase()], - ]) - ) - ), - `Shows correct device types for ${platform === ENUMS.PLATFORM.IOS ? 'iOS' : 'Android'} with supported types: ${supportedDeviceTypes || 'Phone'}` - ); + .hasText(t('cancel')); } ); - - test('test selects random phone device type and version if any device is selected and resets after scan starts', async function (assert) { - assert.expect(); - - const isIOS = ENUMS.PLATFORM.IOS === this.project.platform; - - const file = this.server.create('file', { - project: this.project.id, - profile: '100', - dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, - is_dynamic_done: false, - is_active: true, - min_os_version: '0', // so that we can select any option for version - supported_cpu_architectures: isIOS ? 'arm64' : '', - supported_device_types: isIOS ? 'iPhone, iPad' : '', // required for Ios to show device types - }); - - // update project with latest file - this.project.update({ - last_file_id: file.id, - }); - - // set the file - this.set( - 'file', - this.store.push(this.store.normalize('file', file.toJSON())) - ); - - // server mocks - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; - }); - - this.server.put('/dynamicscan/:id', (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.BOOTING, - }); - - return new Response(200); - }); - - this.server.post( - '/dynamicscan/:id/schedule_automation', - (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.INQUEUE, - }); - - return new Response(201); - } - ); - - this.server.put('/profiles/:id/device_preference', (schema, req) => { - const data = req.requestBody - .split('&') - .map((it) => it.split('=')) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); - - this.set('requestBody', data); - - // Preference should be reset after dynamic scan enters an in progress state - if (this.checkPreferenceReset) { - const windowService = this.owner.lookup('service:browser/window'); - - const actualDevicePrefData = JSON.parse( - windowService.localStorage.getItem('actualDevicePrefData') - ); - - assert.strictEqual( - data.device_type, - String(actualDevicePrefData.device_type) - ); - - assert.strictEqual( - data.platform_version, - String(actualDevicePrefData.platform_version) - ); - } - // When dynamic scan is started, the phone device type is selected with an random device version - else if (this.verifyPreferenceChange) { - assert.notEqual(data.platform_version, '0'); // Device OS version should not be any device - - assert.strictEqual( - data.device_type, - String(ENUMS.DEVICE_TYPE.PHONE_REQUIRED) - ); - - this.set('checkPreferenceReset', true); - } - - return new Response(200); - }); - - await render(hbs` - - - - `); - - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); - - const deviceTypeTrigger = `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}`; - - const anyDeviceTypeLabel = t( - deviceType([ENUMS.DEVICE_TYPE.NO_PREFERENCE]) - ); - - // Open device type dropdown - await click(deviceTypeTrigger); - - await selectChoose(deviceTypeTrigger, anyDeviceTypeLabel); - - assert.dom(deviceTypeTrigger).hasText(anyDeviceTypeLabel); - - const osVersionSelectTrigger = `[data-test-projectPreference-osVersionSelect] .${classes.trigger}`; - const anyOSVersionLabel = t('anyVersion'); - - // Open OS version dropdown - await click(osVersionSelectTrigger); - - await selectChoose(osVersionSelectTrigger, anyOSVersionLabel); - - // verify ui - assert.dom(osVersionSelectTrigger).hasText(anyOSVersionLabel); - - this.set('verifyPreferenceChange', true); - - assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]') - .isNotDisabled() - .hasText(t('modalCard.dynamicScan.start')); - - await click('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]'); - - const notify = this.owner.lookup('service:notifications'); - const poll = this.owner.lookup('service:poll'); - - assert.strictEqual(notify.successMsg, t('startingScan')); - - // simulate polling - if (poll.callback) { - await poll.callback(); - } - - // modal should close - assert.dom('[data-test-ak-appbar]').doesNotExist(); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .doesNotExist(); - - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip]') - .exists() - .hasText(dynamicScanStatusText()[ENUMS.DYNAMIC_STATUS.BOOTING]); - - // Preference should be deleted from local storage - const windowService = this.owner.lookup('service:browser/window'); - - const actualDevicePrefData = JSON.parse( - windowService.localStorage.getItem('actualDevicePrefData') - ); - - assert.notOk(actualDevicePrefData); - }); } ); diff --git a/tests/integration/components/file-details/proxy-settings-test.js b/tests/integration/components/file-details/proxy-settings-test.js new file mode 100644 index 000000000..33e8ac681 --- /dev/null +++ b/tests/integration/components/file-details/proxy-settings-test.js @@ -0,0 +1,208 @@ +import Service from '@ember/service'; +import { click, find, render, triggerEvent } from '@ember/test-helpers'; +import { faker } from '@faker-js/faker'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupIntl, t } from 'ember-intl/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import { Response } from 'miragejs'; +import { module, test } from 'qunit'; + +class NotificationsStub extends Service { + errorMsg = null; + infoMsg = null; + + error(msg) { + this.errorMsg = msg; + } + + info(msg) { + this.infoMsg = msg; + } +} + +module( + 'Integration | Component | file-details/proxy-settings', + function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks, 'en'); + + hooks.beforeEach(async function () { + const store = this.owner.lookup('service:store'); + + const profile = this.server.create('profile', { id: '100' }); + + const file = this.server.create('file', { + project: '1', + profile: profile.id, + is_active: true, + }); + + this.server.create('project', { + last_file_id: file.id, + id: '1', + }); + + // server mocks + this.server.get('/profiles/:id', (schema, req) => { + return schema.profiles.find(`${req.params.id}`).toJSON(); + }); + + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`).toJSON(); + }); + + this.owner.register('service:notifications', NotificationsStub); + + this.setProperties({ + file: store.push(store.normalize('file', file.toJSON())), + store, + }); + }); + + test('it renders proxy settings', async function (assert) { + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: faker.internet.ip(), + port: faker.internet.port(), + enabled: false, + }; + }); + + await render( + hbs`` + ); + + const proxySetting = this.store.peekRecord( + 'proxy-setting', + this.file.profile.get('id') + ); + + assert.notOk(proxySetting.enabled); + + assert.dom('[data-test-fileDetails-proxySettings-container]').exists(); + + assert + .dom('[data-test-fileDetails-proxySettings-enableApiProxyLabel]') + .hasText(`${t('enable')} ${t('proxySettingsTitle')}`); + + const proxySettingsToggle = + '[data-test-fileDetails-proxySettings-enableApiProxyToggle] [data-test-toggle-input]'; + + assert.dom(proxySettingsToggle).isNotDisabled().isNotChecked(); + + const proxySettingsTooltip = find( + '[data-test-fileDetails-proxySettings-helpIcon]' + ); + + await triggerEvent(proxySettingsTooltip, 'mouseenter'); + + assert + .dom('[data-test-fileDetails-proxySettings-helpTooltipContent]') + .exists() + .containsText(t('proxySettingsRouteVia')) + .containsText(proxySetting.port) + .containsText(proxySetting.host); + + await triggerEvent(proxySettingsTooltip, 'mouseleave'); + }); + + test.each( + 'it enables api proxy', + [ + { enabled: false, assertions: 8 }, + { enabled: true, assertions: 8 }, + { enabled: false, assertions: 6, fail: true }, + ], + async function (assert, { enabled, assertions, fail }) { + assert.expect(assertions); + + this.server.get('/profiles/:id/proxy_settings', (_, req) => { + return { + id: req.params.id, + host: faker.internet.ip(), + port: faker.internet.port(), + enabled, + }; + }); + + this.server.put('/profiles/:id/proxy_settings', (_, req) => { + if (fail) { + return new Response( + 501, + {}, + { detail: 'failed to update proxy settings' } + ); + } + + const data = JSON.parse(req.requestBody); + + if (enabled) { + assert.false(data.enabled); + } else { + assert.true(data.enabled); + } + + return { + id: req.params.id, + ...data, + }; + }); + + await render( + hbs`` + ); + + const proxySetting = this.store.peekRecord( + 'proxy-setting', + this.file.profile.get('id') + ); + + assert + .dom('[data-test-fileDetails-proxySettings-enableApiProxyLabel]') + .hasText(`${t('enable')} ${t('proxySettingsTitle')}`); + + const proxySettingsToggle = + '[data-test-fileDetails-proxySettings-enableApiProxyToggle] [data-test-toggle-input]'; + + if (enabled) { + assert.dom(proxySettingsToggle).isNotDisabled().isChecked(); + } else { + assert.dom(proxySettingsToggle).isNotDisabled().isNotChecked(); + } + + await click(proxySettingsToggle); + + const notify = this.owner.lookup('service:notifications'); + + if (fail) { + assert.dom(proxySettingsToggle).isNotDisabled().isNotChecked(); + + assert.strictEqual( + notify.errorMsg, + 'failed to update proxy settings' + ); + } else { + if (enabled) { + assert.dom(proxySettingsToggle).isNotDisabled().isNotChecked(); + } else { + assert.dom(proxySettingsToggle).isNotDisabled().isChecked(); + } + + if (enabled) { + assert.false(proxySetting.enabled); + } else { + assert.true(proxySetting.enabled); + } + + assert.strictEqual( + notify.infoMsg, + `${t('proxyTurned')} ${(enabled ? t('off') : t('on')).toUpperCase()}` + ); + } + } + ); + } +); diff --git a/tests/unit/adapters/available-automated-device-test.js b/tests/unit/adapters/available-automated-device-test.js new file mode 100644 index 000000000..a0d4238ae --- /dev/null +++ b/tests/unit/adapters/available-automated-device-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Adapter | available automated device', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let adapter = this.owner.lookup('adapter:available-automated-device'); + assert.ok(adapter); + }); +}); diff --git a/tests/unit/adapters/available-manual-device-test.js b/tests/unit/adapters/available-manual-device-test.js new file mode 100644 index 000000000..03e7614a6 --- /dev/null +++ b/tests/unit/adapters/available-manual-device-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Adapter | available manual device', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let adapter = this.owner.lookup('adapter:available-manual-device'); + assert.ok(adapter); + }); +}); diff --git a/tests/unit/adapters/commondrf-nested-test.js b/tests/unit/adapters/commondrf-nested-test.js new file mode 100644 index 000000000..8d80e3baf --- /dev/null +++ b/tests/unit/adapters/commondrf-nested-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Adapter | commondrf nested', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let adapter = this.owner.lookup('adapter:commondrf-nested'); + assert.ok(adapter); + }); +}); diff --git a/tests/unit/adapters/ds-automated-device-preference-test.js b/tests/unit/adapters/ds-automated-device-preference-test.js new file mode 100644 index 000000000..c045d2000 --- /dev/null +++ b/tests/unit/adapters/ds-automated-device-preference-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Adapter | ds automated device preference', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let adapter = this.owner.lookup('adapter:ds-automated-device-preference'); + assert.ok(adapter); + }); +}); diff --git a/tests/unit/adapters/ds-automation-preference-test.js b/tests/unit/adapters/ds-automation-preference-test.js new file mode 100644 index 000000000..a88fd3f07 --- /dev/null +++ b/tests/unit/adapters/ds-automation-preference-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Adapter | ds automation preference', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let adapter = this.owner.lookup('adapter:ds-automation-preference'); + assert.ok(adapter); + }); +}); diff --git a/tests/unit/adapters/ds-manual-device-preference-test.js b/tests/unit/adapters/ds-manual-device-preference-test.js new file mode 100644 index 000000000..d449ccae9 --- /dev/null +++ b/tests/unit/adapters/ds-manual-device-preference-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Adapter | ds manual device preference', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let adapter = this.owner.lookup('adapter:ds-manual-device-preference'); + assert.ok(adapter); + }); +}); diff --git a/tests/unit/controllers/authenticated/dashboard/file/dynamic-scan/automated-test.js b/tests/unit/controllers/authenticated/dashboard/file/dynamic-scan/automated-test.js new file mode 100644 index 000000000..c7bec89ce --- /dev/null +++ b/tests/unit/controllers/authenticated/dashboard/file/dynamic-scan/automated-test.js @@ -0,0 +1,17 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module( + 'Unit | Controller | authenticated/dashboard/file/dynamic-scan/automated', + function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let controller = this.owner.lookup( + 'controller:authenticated/dashboard/file/dynamic-scan/automated' + ); + assert.ok(controller); + }); + } +); diff --git a/tests/unit/models/available-automated-device-test.js b/tests/unit/models/available-automated-device-test.js new file mode 100644 index 000000000..ac774aead --- /dev/null +++ b/tests/unit/models/available-automated-device-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | available automated device', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let store = this.owner.lookup('service:store'); + let model = store.createRecord('available-automated-device', {}); + assert.ok(model); + }); +}); diff --git a/tests/unit/models/available-manual-device-test.js b/tests/unit/models/available-manual-device-test.js new file mode 100644 index 000000000..acb581fec --- /dev/null +++ b/tests/unit/models/available-manual-device-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | available manual device', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let store = this.owner.lookup('service:store'); + let model = store.createRecord('available-manual-device', {}); + assert.ok(model); + }); +}); diff --git a/tests/unit/models/ds-automated-device-preference-test.js b/tests/unit/models/ds-automated-device-preference-test.js new file mode 100644 index 000000000..a219843cc --- /dev/null +++ b/tests/unit/models/ds-automated-device-preference-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | ds automated device preference', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let store = this.owner.lookup('service:store'); + let model = store.createRecord('ds-automated-device-preference', {}); + assert.ok(model); + }); +}); diff --git a/tests/unit/models/ds-automation-preference-test.js b/tests/unit/models/ds-automation-preference-test.js new file mode 100644 index 000000000..90f576daf --- /dev/null +++ b/tests/unit/models/ds-automation-preference-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | ds automation preference', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let store = this.owner.lookup('service:store'); + let model = store.createRecord('ds-automation-preference', {}); + assert.ok(model); + }); +}); diff --git a/tests/unit/models/ds-manual-device-preference-test.js b/tests/unit/models/ds-manual-device-preference-test.js new file mode 100644 index 000000000..2722b5c38 --- /dev/null +++ b/tests/unit/models/ds-manual-device-preference-test.js @@ -0,0 +1,13 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | ds manual device preference', function (hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function (assert) { + let store = this.owner.lookup('service:store'); + let model = store.createRecord('ds-manual-device-preference', {}); + assert.ok(model); + }); +}); diff --git a/translations/en.json b/translations/en.json index 2d965a10f..3aba1fcc7 100644 --- a/translations/en.json +++ b/translations/en.json @@ -167,6 +167,7 @@ "businessImplication": "Business Implication", "by": "by", "cancel": "Cancel", + "cancelled": "Cancelled", "cancelScan": "Cancel Scan", "cancelSubsciption": "Cancel Subscription", "capturedApiEmptyStepsLabel": "Steps to capture API's", @@ -1272,6 +1273,7 @@ "riskOf": "Risk Of", "riskType": "Risk Type", "role": "Role", + "running": "Running", "sama": "SAMA", "samaExpansion": "Saudi Arabian Monetary Authority", "samlAuth": "SAML Authentication", diff --git a/translations/ja.json b/translations/ja.json index a64974cba..d68afe294 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -167,6 +167,7 @@ "businessImplication": "ビジネスへの影響", "by": "by", "cancel": "キャンセル", + "cancelled": "Cancelled", "cancelScan": "Cancel Scan", "cancelSubsciption": "サブスクリプションのキャンセル", "capturedApiEmptyStepsLabel": "Steps to capture API's", @@ -1272,6 +1273,7 @@ "riskOf": "リスク", "riskType": "リスクタイプ", "role": "役割", + "running": "Running", "sama": "SAMA", "samaExpansion": "Saudi Arabian Monetary Authority", "samlAuth": "SAML Authentication",