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..34d139935 100644 --- a/app/adapters/file.ts +++ b/app/adapters/file.ts @@ -1,25 +1,51 @@ -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, + isScheduledScan: boolean + ): Promise { + let url = `${this._buildURL('file', fileId)}/dynamicscans?limit=1&mode=${mode}`; + + // Add scheduled scan filters + if (isScheduledScan) { + url = url.concat('&engine=2&group_status=running'); + } + + 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..ee45395dc --- /dev/null +++ b/app/components/ds-preference-provider/index.ts @@ -0,0 +1,187 @@ +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(); + } + + @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..5b139bba5 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 @@ -6,15 +6,15 @@ import { tracked } from 'tracked-built-ins'; import { task } from 'ember-concurrency'; import { inject as service } from '@ember/service'; import { isEmpty } from '@ember/utils'; +import type IntlService from 'ember-intl/services/intl'; +import type Store from '@ember-data/store'; import parseError from 'irene/utils/parse-error'; import ENUMS from 'irene/enums'; 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 { deviceType } from 'irene/helpers/device-type'; 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; }; } @@ -93,11 +92,18 @@ export default class FileDetailsDynamicScanDrawerAutomatedDastComponent extends return this.dpContext.dsAutomatedDevicePreference; } + get isAnyDevicePrefSelected() { + return ( + this.automatedDastDevicePreferences?.dsAutomatedDeviceSelection === + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.ANY_DEVICE + ); + } + get minOSVersion() { const version = - this.automatedDastDevicePreferences?.ds_automated_platform_version_min; + this.automatedDastDevicePreferences?.dsAutomatedPlatformVersionMin; - return isEmpty(version) ? '-' : version; + return isEmpty(version) ? this.intl.t('anyVersion') : version; } get devicePrefInfoData() { @@ -108,18 +114,31 @@ export default class FileDetailsDynamicScanDrawerAutomatedDastComponent extends value: this.intl.t( dsAutomatedDevicePref([ Number( - this.automatedDastDevicePreferences - ?.ds_automated_device_selection || - ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA + this.automatedDastDevicePreferences?.dsAutomatedDeviceSelection ?? + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.ANY_DEVICE ), ]) ), }, - { - id: 'minOSVersion', - title: this.intl.t('minOSVersion'), - value: this.minOSVersion, - }, + ...(this.isAnyDevicePrefSelected + ? [] + : [ + { + id: 'deviceType', + title: this.intl.t('deviceType'), + value: this.intl.t( + deviceType([ + this.automatedDastDevicePreferences?.dsAutomatedDeviceType ?? + ENUMS.DS_AUTOMATED_DEVICE_TYPE.NO_PREFERENCE, + ]) + ), + }, + { + id: 'minOSVersion', + title: this.intl.t('minOSVersion'), + value: this.minOSVersion, + }, + ]), ]; } 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..38fc1c5d6 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,30 @@ - {{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}} = 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, + }, ]; } @@ -126,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 43dcd84dd..a2bd0f153 100644 --- a/app/components/file-details/dynamic-scan/action/drawer/index.ts +++ b/app/components/file-details/dynamic-scan/action/drawer/index.ts @@ -9,23 +9,23 @@ 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 AvailableManualDeviceModel from 'irene/models/available-manual-device'; +import type { DsPreferenceContext } from 'irene/components/ds-preference-provider'; import type IreneAjaxService from 'irene/services/ajax'; export interface FileDetailsDynamicScanActionDrawerSignature { Args: { + dpContext: DsPreferenceContext; onClose: () => void; - pollDynamicStatus: () => void; + onScanStart: (dynamicscan: DynamicscanModel) => void; file: FileModel; isAutomatedScan?: boolean; - dpContext: DevicePreferenceContext; }; } @@ -35,8 +35,8 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone @service declare store: Store; @service('notifications') declare notify: NotificationService; - @tracked isApiScanEnabled = false; - @tracked allAvailableManualDevices: ProjectAvailableDeviceModel[] = []; + @tracked isApiCaptureEnabled = false; + @tracked availableManualDevices: AvailableManualDeviceModel[] = []; constructor( owner: unknown, @@ -45,7 +45,7 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone super(owner, args); if (!this.args.isAutomatedScan) { - this.fetchAllAvailableManualDevices.perform(); + this.fetchAvailableManualDevices.perform(); } } @@ -57,6 +57,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'); } @@ -70,13 +74,13 @@ 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( + this.availableManualDevices?.findIndex( (d) => d.deviceIdentifier === this.dsManualDeviceIdentifier ) !== -1 ); @@ -92,7 +96,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 || @@ -106,8 +110,8 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone } @action - enableApiScan(_: Event, checked: boolean) { - this.isApiScanEnabled = !!checked; + handleApiCaptureChange(_: Event, checked: boolean) { + this.isApiCaptureEnabled = !!checked; } @action @@ -120,15 +124,33 @@ export default class FileDetailsDynamicScanActionDrawerComponent extends Compone this.startDynamicScan.perform(); } + fetchAvailableManualDevices = task(async () => { + try { + const adapter = this.store.adapterFor('available-manual-device'); + + adapter.setNestedUrlNamespace( + this.args.file.project?.get('id') as string + ); + + const devices = await this.store.query('available-manual-device', { + platform_version_min: this.args.file.minOsVersion, + }); + + this.availableManualDevices = devices.slice(); + } catch (error) { + this.notify.error(this.intl.t('errorFetchingAvailableDevices')); + } + }); + 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 = [ @@ -137,13 +159,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 as object); - 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) { @@ -152,24 +177,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..63d06de51 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,32 +1,46 @@ +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; }; } export default class FileDetailsDynamicScanDrawerManualDastComponent extends Component { deviceSelectionTypes = ENUMS.DS_MANUAL_DEVICE_SELECTION.BASE_CHOICES; + willDestroy(): void { + super.willDestroy(); + + if (this.dpContext.dsManualDevicePreference?.hasDirtyAttributes) { + this.dpContext.dsManualDevicePreference.rollbackAttributes(); + } + } + get file() { 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 +67,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..a02e86ede 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) { - return; - } + this.handleExtendTimeMenuClose(); try { - await dynamicscan.extendTime(time); + await this.dynamicscan.extendTime(time); + + await this.dynamicscan.reload(); } catch (error) { const err = error as AdapterError; @@ -158,8 +123,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 426073282..a210e8dd0 100644 --- a/app/components/file-details/dynamic-scan/action/index.ts +++ b/app/components/file-details/dynamic-scan/action/index.ts @@ -3,42 +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'; -import type IreneAjaxService from 'irene/services/ajax'; -import type { AjaxError } from 'irene/services/ajax'; 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: IreneAjaxService; + @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; } @@ -48,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 @@ -66,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 AjaxError).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..2761b2acf 100644 --- a/app/components/file-details/dynamic-scan/automated/index.hbs +++ b/app/components/file-details/dynamic-scan/automated/index.hbs @@ -1,129 +1,86 @@ -{{#if this.fetchDynamicscan.isRunning}} - - - - - {{t 'loading'}}... - - -{{else if (and @file.canRunAutomatedDynamicscan this.automationEnabled)}} - +{{#if this.dynamicscanAutomationFeatureAvailable}} + {{#if this.getDsAutomationPreference.isRunning}} - - {{t 'realDevice'}} - + - + + {{t 'loading'}}... + + + {{else if this.automationPreference.dynamicScanAutomationEnabled}} + + <:statusChip> + + <:actionButton> + - - - - - - - {{#if this.isFullscreenView}} - - - - - {{else}} - + <:default> - - {{/if}} - - -{{else if (and @file.canRunAutomatedDynamicscan (not this.automationEnabled))}} - - - - + + {{else}} + - {{t 'toggleAutomatedDAST'}} - + - - {{! TODO: Get the correct text for this description }} - lorem ipsum dolor sit amet consectetur adipiscing - + + {{t 'toggleAutomatedDAST'}} + - - {{t 'goToSettings'}} - - + + {{t 'toggleAutomatedDASTDesc'}} + + + {{t 'goToSettings'}} + + + {{/if}} {{else}} diff --git a/app/components/file-details/dynamic-scan/automated/index.scss b/app/components/file-details/dynamic-scan/automated/index.scss index 6d18820cf..159226866 100644 --- a/app/components/file-details/dynamic-scan/automated/index.scss +++ b/app/components/file-details/dynamic-scan/automated/index.scss @@ -1,14 +1,3 @@ -.automated-dast-container { - background-color: var(--file-details-dynamic-scan-automated-background-main); - border: 1px solid var(--file-details-dynamic-scan-automated-border-color); -} - -.automated-dast-header { - border-bottom: 1px solid - var(--file-details-dynamic-scan-automated-border-color); - padding: 0.5em 1.5em; -} - .automated-dast-disabled-card { width: 518px; background-color: var(--file-details-dynamic-scan-automated-background-main); diff --git a/app/components/file-details/dynamic-scan/automated/index.ts b/app/components/file-details/dynamic-scan/automated/index.ts index de36ead97..454250a22 100644 --- a/app/components/file-details/dynamic-scan/automated/index.ts +++ b/app/components/file-details/dynamic-scan/automated/index.ts @@ -3,7 +3,6 @@ import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { task } from 'ember-concurrency'; import { inject as service } from '@ember/service'; -import { waitForPromise } from '@ember/test-waiters'; import type IntlService from 'ember-intl/services/intl'; import type RouterService from '@ember/routing/router-service'; import type Store from '@ember-data/store'; @@ -11,6 +10,9 @@ import type Store from '@ember-data/store'; 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'; +import type DynamicScanService from 'irene/services/dynamic-scan'; export interface FileDetailsDastAutomatedSignature { Args: { @@ -23,27 +25,28 @@ export default class FileDetailsDastAutomated extends Component { + @action + handleStartScan(dynamicScan: DynamicscanModel) { + this.dsService.automatedScan = dynamicScan; + } + + @action + handleScanShutdown() { + this.dsService.fetchLatestAutomatedScan.perform(this.args.file); + } + + getDsAutomationPreference = task(async () => { 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; - - try { - this.dynamicScan = await this.store.findRecord('dynamicscan', id); - } catch (e) { - this.notify.error(parseError(e, this.intl.t('pleaseTryAgain'))); - } - }); } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/file-details/dynamic-scan/device-wrapper/index.hbs b/app/components/file-details/dynamic-scan/device-wrapper/index.hbs new file mode 100644 index 000000000..e69dc9398 --- /dev/null +++ b/app/components/file-details/dynamic-scan/device-wrapper/index.hbs @@ -0,0 +1,114 @@ + + + + {{t 'realDevice'}} + + + {{#if @loadingScanStatus}} + + {{else}} + + {{#if @showStatusChip}} + {{yield to='statusChip'}} + {{/if}} + + {{#if @showActionButton}} + {{yield + (hash closeFullscreen=this.handleFullscreenClose) + to='actionButton' + }} + {{/if}} + + {{#if @isFullscreenSupported}} + + + + {{/if}} + + {{/if}} + + +
+ {{#if this.isFullscreenView}} + + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/manual/index.scss b/app/components/file-details/dynamic-scan/device-wrapper/index.scss similarity index 63% rename from app/components/file-details/dynamic-scan/manual/index.scss rename to app/components/file-details/dynamic-scan/device-wrapper/index.scss index 264213488..f236d910c 100644 --- a/app/components/file-details/dynamic-scan/manual/index.scss +++ b/app/components/file-details/dynamic-scan/device-wrapper/index.scss @@ -1,15 +1,18 @@ -.manual-dast-container { - background-color: var(--file-details-dynamic-scan-manual-background-main); - border: 1px solid var(--file-details-dynamic-scan-manual-border-color); +.device-wrapper-container { + background-color: var( + --file-details-dynamic-scan-device-wrapper-background-main + ); + border: 1px solid var(--file-details-dynamic-scan-device-wrapper-border-color); } -.manual-dast-header { - border-bottom: 1px solid var(--file-details-dynamic-scan-manual-border-color); +.device-wrapper-header { + border-bottom: 1px solid + var(--file-details-dynamic-scan-device-wrapper-border-color); padding: 0.5em 1.5em; min-height: 48px; } -.vnc-viewer-fullscreen { +.device-viewer-fullscreen { position: fixed; inset: 0px; z-index: var(--vnc-viewer-modal-zIndex); @@ -19,7 +22,7 @@ justify-content: center; } -.vnc-viewer-backdrop { +.device-viewer-backdrop { position: fixed; display: flex; align-items: center; @@ -29,7 +32,7 @@ z-index: -1; } -.vnc-viewer-fullscreen-container { +.device-viewer-fullscreen-container { width: 450px; background-color: var(--vnc-viewer-modal-background); color: var(--vnc-viewer-modal-text-color); diff --git a/app/components/file-details/dynamic-scan/device-wrapper/index.ts b/app/components/file-details/dynamic-scan/device-wrapper/index.ts new file mode 100644 index 000000000..f8ea5144c --- /dev/null +++ b/app/components/file-details/dynamic-scan/device-wrapper/index.ts @@ -0,0 +1,37 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +interface DeviceWrapperSignature { + Args: { + showStatusChip?: boolean; + showActionButton?: boolean; + loadingScanStatus?: boolean; + isFullscreenSupported?: boolean; + }; + Blocks: { + statusChip: []; + actionButton: [{ closeFullscreen: () => void }]; + default: [{ isFullscreen: boolean; closeFullscreen: () => void }]; + }; +} + +export default class FileDetailsDynamicScanDeviceWrapper extends Component { + @tracked isFullscreenView = false; + + @action + handleFullscreenClose() { + this.isFullscreenView = false; + } + + @action + toggleFullscreenView() { + this.isFullscreenView = !this.isFullscreenView; + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::DeviceWrapper': typeof FileDetailsDynamicScanDeviceWrapper; + } +} diff --git a/app/components/file-details/dynamic-scan/header/index.hbs b/app/components/file-details/dynamic-scan/header/index.hbs index 1a1d872b7..0665256b0 100644 --- a/app/components/file-details/dynamic-scan/header/index.hbs +++ b/app/components/file-details/dynamic-scan/header/index.hbs @@ -14,7 +14,7 @@ @route={{item.route}} @currentWhen={{item.activeRoutes}} @indicatorVariant='shadow' - @hasBadge={{if (or item.inProgress item.count) true false}} + @hasBadge={{this.showTabIcon item}} @badgeBackground={{if item.count true false}} > <:badge> @@ -33,8 +33,19 @@ {{/if}} + {{#if item.inProgress}} - + + {{else if item.iconDetails}} + {{/if}} diff --git a/app/components/file-details/dynamic-scan/header/index.ts b/app/components/file-details/dynamic-scan/header/index.ts index e6c216ba3..fa9fe8097 100644 --- a/app/components/file-details/dynamic-scan/header/index.ts +++ b/app/components/file-details/dynamic-scan/header/index.ts @@ -1,25 +1,37 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; 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 type DynamicScanService from 'irene/services/dynamic-scan'; +import { DsComputedStatus } from 'irene/models/dynamicscan'; + +interface TabItem { + id: string; + label: string; + route: string; + activeRoutes: string; + iconDetails?: { icon: string; color: 'success' | 'warn' } | null; + inProgress?: boolean; + count?: number; + isActive?: boolean; +} export interface FileDetailsDastHeaderSignature { Args: { file: FileModel; profileId: number; - dynamicScan: DynamicscanModel | null; }; } export default class FileDetailsDastHeader extends Component { @service declare intl: IntlService; @service declare router: RouterService; - @service declare store: Store; - @service('notifications') declare notify: NotificationService; + @service declare configuration: ConfigurationService; + @service('dynamic-scan') declare dsService: DynamicScanService; get file() { return this.args.file; @@ -33,8 +45,19 @@ export default class FileDetailsDastHeader extends Component + {{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..09a5db258 100644 --- a/app/components/file-details/dynamic-scan/index.ts +++ b/app/components/file-details/dynamic-scan/index.ts @@ -1,13 +1,8 @@ -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 Component from '@glimmer/component'; -import parseError from 'irene/utils/parse-error'; -import type DynamicscanModel from 'irene/models/dynamicscan'; import type FileModel from 'irene/models/file'; +import type DynamicScanService from 'irene/services/dynamic-scan'; interface DynamicScanSignature { Args: { @@ -20,21 +15,19 @@ interface DynamicScanSignature { } export default class DynamicScan extends Component { - @service declare store: Store; - @service declare intl: IntlService; - @service('notifications') declare notify: NotificationService; + @service declare dynamicScan: DynamicScanService; - @tracked dynamicScan: DynamicscanModel | null = null; + constructor(owner: unknown, args: DynamicScanSignature['Args']) { + super(owner, args); - fetchDynamicscan = task(async () => { - const id = this.args.profileId; + this.dynamicScan.fetchLatestScans(this.args.file); + } - try { - this.dynamicScan = await this.store.findRecord('dynamicscan', id); - } catch (e) { - this.notify.error(parseError(e, this.intl.t('pleaseTryAgain'))); - } - }); + willDestroy(): void { + super.willDestroy(); + + this.dynamicScan.resetScans(); + } } declare module '@glint/environment-ember-loose/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..d06166b80 100644 --- a/app/components/file-details/dynamic-scan/manual/index.hbs +++ b/app/components/file-details/dynamic-scan/manual/index.hbs @@ -1,129 +1,43 @@ - - - + + + + <:actionButton as |dw|> + + + + <:default as |dw|> + - {{t 'realDevice'}} - - - - {{#if this.showStatusChip}} - - - {{/if}} - - {{#if this.showActionButton}} - - {{/if}} - - {{#if (and @file.isReady (not this.isFullscreenView))}} - - - - {{/if}} - - - -
- {{#if this.isFullscreenView}} - - \ No newline at end of file + <:controls> + {{#if dw.isFullscreen}} + + {{/if}} + + + + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/manual/index.ts b/app/components/file-details/dynamic-scan/manual/index.ts index 17f4cadaf..8a49a8e12 100644 --- a/app/components/file-details/dynamic-scan/manual/index.ts +++ b/app/components/file-details/dynamic-scan/manual/index.ts @@ -1,91 +1,48 @@ import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; 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'; -import parseError from 'irene/utils/parse-error'; +import type DynamicScanService from 'irene/services/dynamic-scan'; import type DynamicscanModel from 'irene/models/dynamicscan'; import type FileModel from 'irene/models/file'; -import type { DevicePreferenceContext } from 'irene/components/project-preferences-old/provider'; export interface FileDetailsDastManualSignature { Args: { file: FileModel; profileId: number; - dpContext: DevicePreferenceContext; }; } export default class FileDetailsDastManual extends Component { - @service declare intl: IntlService; - @service declare store: Store; - @service('browser/window') declare window: Window; - @service('notifications') declare notify: NotificationService; + @service('dynamic-scan') declare dsService: DynamicScanService; - @tracked isFullscreenView = false; - @tracked devicePrefObserverRegistered = false; - @tracked dynamicScan: DynamicscanModel | null = null; - - constructor(owner: unknown, args: FileDetailsDastManualSignature['Args']) { - super(owner, args); - - // TODO: Uncomment when full DAST feature is ready. - // this.fetchDynamicscan.perform(); + get dynamicScan() { + return this.dsService.manualScan; } - get file() { - return this.args.file; + get isFetchingDynamicScan() { + return this.dsService.fetchLatestManualScan.isRunning; } get showStatusChip() { - if (this.file?.isDynamicStatusReady) { - return false; - } else if ( - this.file?.isDynamicStatusNoneOrError || - this.file?.isDynamicStatusInProgress - ) { - return true; - } - - return false; + return !this.dynamicScan?.isReady; } get showActionButton() { - if (this.isFullscreenView) { - return false; - } - - if (this.file?.isDynamicStatusReady || this.file?.isDynamicStatusError) { - return true; - } else if (this.file?.isDynamicStatusInProgress) { - return false; - } - - return true; + return !this.dynamicScan?.isShuttingDown; } @action - handleFullscreenClose() { - this.isFullscreenView = false; + handleScanStart(dynamicScan: DynamicscanModel) { + this.dsService.manualScan = dynamicScan; } @action - toggleFullscreenView() { - this.isFullscreenView = !this.isFullscreenView; - } + handleScanShutdown(closeFullscreen: () => void) { + closeFullscreen(); - 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'))); - } - }); + this.dsService.fetchLatestManualScan.perform(this.args.file); + } } declare module '@glint/environment-ember-loose/registry' { diff --git a/app/components/file-details/dynamic-scan/scheduled-automated/index.hbs b/app/components/file-details/dynamic-scan/scheduled-automated/index.hbs new file mode 100644 index 000000000..b2f3c7a80 --- /dev/null +++ b/app/components/file-details/dynamic-scan/scheduled-automated/index.hbs @@ -0,0 +1,43 @@ + + <:statusChip> + + + + <:actionButton> + + + + <:default as |dw|> + + <:controls> + {{#if dw.isFullscreen}} + + {{/if}} + + + + \ No newline at end of file diff --git a/app/components/file-details/dynamic-scan/scheduled-automated/index.ts b/app/components/file-details/dynamic-scan/scheduled-automated/index.ts new file mode 100644 index 000000000..85bf486ca --- /dev/null +++ b/app/components/file-details/dynamic-scan/scheduled-automated/index.ts @@ -0,0 +1,56 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import type RouterService from '@ember/routing/router-service'; + +import type DynamicScanService from 'irene/services/dynamic-scan'; +import type FileModel from 'irene/models/file'; + +export interface FileDetailsDynamicScanScheduledAutomatedSignature { + Args: { + file: FileModel; + profileId: number; + }; +} + +export default class FileDetailsDynamicScanScheduledAutomatedComponent extends Component { + @service declare router: RouterService; + @service('dynamic-scan') declare dsService: DynamicScanService; + + get dynamicScan() { + return this.dsService.scheduledScan; + } + + get isFetchingDynamicScan() { + return this.dsService.fetchLatestScheduledScan.isRunning; + } + + get showStatusChip() { + return !this.dynamicScan?.isReady; + } + + get showActionButton() { + return !this.dynamicScan?.isShuttingDown; + } + + // cannot start from here in scheduled automated + handleScanStart() {} + + @action + handleScanShutdown() { + this.router.transitionTo( + 'authenticated.dashboard.file.dynamic-scan.automated' + ); + + // this will make showScheduledScan false + this.dsService.scheduledScan = null; + + this.dsService.fetchLatestAutomatedScan.perform(this.args.file); + } +} + +declare module '@glint/environment-ember-loose/registry' { + export default interface Registry { + 'FileDetails::DynamicScan::ScheduledAutomated': typeof FileDetailsDynamicScanScheduledAutomatedComponent; + } +} 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..a9a90e798 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,16 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import { action } from '@ember/object'; +import { waitForPromise } from '@ember/test-waiters'; 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'; export interface FileDetailsScanActionsDynamicScanSignature { Args: { @@ -12,18 +20,88 @@ 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 both 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 either scans are completed, return completed + if ( + s1 === DsComputedStatus.COMPLETED || + s2 === DsComputedStatus.COMPLETED + ) { + return DsComputedStatus.COMPLETED; + } + + // If either scans are cancelled, return cancelled + if ( + s1 === DsComputedStatus.CANCELLED || + s2 === DsComputedStatus.CANCELLED + ) { + return DsComputedStatus.CANCELLED; + } + + return DsComputedStatus.NOT_STARTED; + } + + fetchLatestManualAutomaticScan = task(async () => { + try { + const file = this.args.file; + + this.automatedDynamicScan = await waitForPromise( + file.getLastDynamicScan(file.id, ENUMS.DYNAMIC_MODE.AUTOMATED) + ); + + this.manualDynamicScan = await waitForPromise( + 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 0a3ea41c2..000000000 --- a/app/components/project-preferences/provider/index.ts +++ /dev/null @@ -1,433 +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'; -import type IreneAjaxService from 'irene/services/ajax'; - -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: IreneAjaxService; - @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..30a7c4c8c 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,109 @@ - - {{#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 +149,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..5e1ed1cef 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 13ac12499..ad6dd0c08 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,14 +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 IreneAjaxService from 'irene/services/ajax'; +import type ProjectModel from 'irene/models/project'; +import type DsAutomationPreferenceModel from 'irene/models/ds-automation-preference'; export interface ProjectSettingsGeneralSettingsDyanmicscanAutomationSettingsSignature { Args: { @@ -22,10 +20,9 @@ export interface ProjectSettingsGeneralSettingsDyanmicscanAutomationSettingsSign export default class ProjectSettingsGeneralSettingsDyanmicscanAutomationSettingsComponent extends Component { @service declare store: Store; @service declare intl: IntlService; - @service declare ajax: IreneAjaxService; @service('notifications') declare notify: NotificationService; - @tracked automationEnabled = false; + @tracked automationPreference: DsAutomationPreferenceModel | null = null; constructor( owner: unknown, @@ -33,19 +30,19 @@ export default class ProjectSettingsGeneralSettingsDyanmicscanAutomationSettings ) { super(owner, args); - this.getDynamicscanMode.perform(); + this.getDsAutomationPreference.perform(); } get profileId() { return this.args.profileId; } - get tAppiumScheduledAutomationSuccessOn() { - return this.intl.t('appiumScheduledAutomationSuccessOn'); + get tScheduledAutomationSuccessOn() { + return this.intl.t('scheduledAutomationSuccessOn'); } - get tAppiumScheduledAutomationSuccessOff() { - return this.intl.t('appiumScheduledAutomationSuccessOff'); + get tScheduledAutomationSuccessOff() { + return this.intl.t('scheduledAutomationSuccessOff'); } get tSomethingWentWrong() { @@ -56,60 +53,38 @@ export default class ProjectSettingsGeneralSettingsDyanmicscanAutomationSettings return this.intl.t('pleaseTryAgain'); } - getDynamicscanMode = task(async () => { + getDsAutomationPreference = 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 () => { + toggleDsAutomationPreference = 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 - ? this.tAppiumScheduledAutomationSuccessOn - : this.tAppiumScheduledAutomationSuccessOff; + const successMsg = enabled + ? this.tScheduledAutomationSuccessOn + : this.tScheduledAutomationSuccessOff; 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..deccae4e6 100644 --- a/app/components/vnc-viewer/index.hbs +++ b/app/components/vnc-viewer/index.hbs @@ -5,44 +5,56 @@ @direction='column' @spacing='3' > - {{!-- {{#if @dynamicScan.isDynamicStatusInProgress}} --}} - {{#if @file.isReady}} - - + {{#if this.isAutomated}} + {{#if + (or + @dynamicScan.isStartingOrShuttingInProgress + @dynamicScan.isReadyOrRunning + ) + }} + + - {{#if (has-block 'controls')}} - {{yield to='controls'}} - {{/if}} - - {{/if}} - - {{#if @file.isDynamicStatusStarting}} - - {{t 'note'}} - - {{t 'dynamicScanText'}} - - {{/if}} + {{#if this.startedBy}} + + {{t 'scanStartedBy'}} + {{this.startedBy}} + + {{else}} + + {{t 'scanTriggeredAutomatically'}} + + {{/if}} + + {{/if}} + {{else}} + {{#if @dynamicScan.isReady}} + + - {{#if (and this.isAutomated @dynamicScan.isDynamicStatusInProgress)}} - - + {{#if (has-block 'controls')}} + {{yield to='controls'}} + {{/if}} + + {{/if}} - {{#if this.startedBy}} - - {{t 'scanStartedBy'}} - {{this.startedBy}} - - {{else}} - - {{t 'scanTriggeredAutomatically'}} - - {{/if}} - + {{#if @dynamicScan.isStarting}} + + {{t 'note'}} - + {{t 'dynamicScanText'}} + + {{/if}} {{/if}}

@@ -60,13 +72,14 @@ {{/if}} {{/if}} -
- {{!-- {{#if (and @dynamicScan.isReadyOrRunning this.deviceType)}} --}} - {{#if (and @file.isReady this.deviceType)}} - {{#if this.isAutomated}} +
+ {{#if this.isAutomated}} + {{#if + (or + @dynamicScan.isStartingOrShuttingInProgress + @dynamicScan.isReadyOrRunning + ) + }} {{t 'note'}} - - {{t 'automatedScanVncNote'}} + {{t + (if + @dynamicScan.isInqueue + 'automatedScanQueuedVncNote' + 'automatedScanRunningVncNote' + ) + }} - - {{else}} - {{/if}} + {{else if @dynamicScan.isReady}} + {{/if}}
diff --git a/app/components/vnc-viewer/index.ts b/app/components/vnc-viewer/index.ts index fdfb7cbd8..da5f0815f 100644 --- a/app/components/vnc-viewer/index.ts +++ b/app/components/vnc-viewer/index.ts @@ -1,8 +1,5 @@ import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; -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'; @@ -11,7 +8,6 @@ import ENV from 'irene/config/environment'; import type FileModel from 'irene/models/file'; import type DynamicscanModel from 'irene/models/dynamicscan'; import type DevicefarmService from 'irene/services/devicefarm'; -import type DevicePreferenceModel from 'irene/models/device-preference'; export interface VncViewerSignature { Args: { @@ -30,60 +26,31 @@ export default class VncViewerComponent extends Component { @service declare store: Store; @service declare devicefarm: DevicefarmService; - @tracked rfb: any = null; - @tracked devicePreference?: DevicePreferenceModel; - deviceFarmPassword = ENV.deviceFarmPassword; - constructor(owner: unknown, args: VncViewerSignature['Args']) { - super(owner, args); - - this.fetchDevicePreference.perform(); - } - get deviceFarmURL() { - const token = this.args.file.deviceToken; - - return this.devicefarm.getTokenizedWSURL(token); - } - - fetchDevicePreference = task(async () => { - const profileId = this.args.profileId; + const token = this.args.dynamicScan?.moriartyDynamicscanToken; - 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 null; + } - return ( - platform === ENUMS.PLATFORM.ANDROID && - deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED - ); + get deviceUsed() { + return this.args.dynamicScan?.deviceUsed; } 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 'nexus5'; + } + + if (platform === ENUMS.PLATFORM.IOS) { + return this.isTablet ? 'ipad black' : 'iphone5s black'; } return ''; @@ -94,12 +61,7 @@ export default class VncViewerComponent extends Component { } get isTablet() { - const deviceType = this.devicePreference?.deviceType; - - return ![ - ENUMS.DEVICE_TYPE.NO_PREFERENCE, - ENUMS.DEVICE_TYPE.PHONE_REQUIRED, - ].includes(deviceType as number); + return this.deviceUsed?.isTablet; } get isIOSDevice() { @@ -113,26 +75,7 @@ export default class VncViewerComponent extends Component { } get startedBy() { - const startedBy = this.dynamicScan?.startedByUser.get('username'); - - return startedBy; - } - - @action - setFocus(focus: boolean) { - const keyboard = this.rfb?.get_keyboard(); - - keyboard.set_focused(focus); - } - - @action - focusKeyboard() { - this.setFocus(true); - } - - @action - blurKeyboard() { - this.setFocus(false); + return this.dynamicScan?.startedByUser?.get('username'); } } 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..df671fb46 --- /dev/null +++ b/app/controllers/authenticated/dashboard/file/dynamic-scan/automated.ts @@ -0,0 +1,49 @@ +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', + 'authenticated.dashboard.file.dynamic-scan.scheduled-automated', + ], + }; + + 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..5829cec00 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', @@ -22,6 +22,7 @@ export default class AuthenticatedDashboardFileDynamicScanManualController exten siblingRoutes: [ 'authenticated.dashboard.file.dynamic-scan.automated', 'authenticated.dashboard.file.dynamic-scan.results', + 'authenticated.dashboard.file.dynamic-scan.scheduled-automated', ], }; diff --git a/app/controllers/authenticated/dashboard/file/dynamic-scan/results.ts b/app/controllers/authenticated/dashboard/file/dynamic-scan/results.ts index 660a40c66..0322641d7 100644 --- a/app/controllers/authenticated/dashboard/file/dynamic-scan/results.ts +++ b/app/controllers/authenticated/dashboard/file/dynamic-scan/results.ts @@ -22,6 +22,7 @@ export default class AuthenticatedDashboardFileDynamicScanResultsController exte siblingRoutes: [ 'authenticated.dashboard.file.dynamic-scan.automated', 'authenticated.dashboard.file.dynamic-scan.manual', + 'authenticated.dashboard.file.dynamic-scan.scheduled-automated', ], }; diff --git a/app/controllers/authenticated/dashboard/file/dynamic-scan/scheduled-automated.ts b/app/controllers/authenticated/dashboard/file/dynamic-scan/scheduled-automated.ts new file mode 100644 index 000000000..0e529c19b --- /dev/null +++ b/app/controllers/authenticated/dashboard/file/dynamic-scan/scheduled-automated.ts @@ -0,0 +1,49 @@ +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 AuthenticatedDashboardFileDynamicScanScheduledAutomatedController 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.scheduledAutomatedDAST'), + route: 'authenticated.dashboard.file.dynamic-scan.scheduled-automated', + models: routeModels, + routeGroup: 'project/files', + + siblingRoutes: [ + 'authenticated.dashboard.file.dynamic-scan.results', + 'authenticated.dashboard.file.dynamic-scan.manual', + 'authenticated.dashboard.file.dynamic-scan.automated', + ], + }; + + 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/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/helpers/device-type.ts b/app/helpers/device-type.ts index 450fb4c9b..afd4c81ad 100644 --- a/app/helpers/device-type.ts +++ b/app/helpers/device-type.ts @@ -5,13 +5,13 @@ export function deviceType(params: [string | number]) { const currentDevice = params[0]; switch (currentDevice) { - case ENUMS.DEVICE_TYPE.NO_PREFERENCE: + case ENUMS.DS_AUTOMATED_DEVICE_TYPE.NO_PREFERENCE: return 'anyDevice'; - case ENUMS.DEVICE_TYPE.PHONE_REQUIRED: + case ENUMS.DS_AUTOMATED_DEVICE_TYPE.PHONE_REQUIRED: return 'phone'; - case ENUMS.DEVICE_TYPE.TABLET_REQUIRED: + case ENUMS.DS_AUTOMATED_DEVICE_TYPE.TABLET_REQUIRED: return 'tablet'; default: diff --git a/app/models/api-scan-options.ts b/app/models/api-scan-options.ts index 897b74252..5f0a3e999 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,17 +9,12 @@ export default class ApiScanOptionsModel extends Model { @attr('string') declare apiUrlFilters: string; - @computed('apiUrlFilters') get apiUrlFilterItems() { - if (!isEmpty(this.apiUrlFilters)) { - return this.apiUrlFilters != null - ? this.apiUrlFilters.split(',') - : undefined; - } + return isEmpty(this.apiUrlFilters) ? [] : this.apiUrlFilters.split(','); } get hasApiUrlFilters() { - return this.apiUrlFilterItems?.length; + return !isEmpty(this.apiUrlFilterItems); } } 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..c5eb37d2d 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.isReadyOrRunning) { + 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.isReadyOrRunning) { + 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..6a3e7c351 100644 --- a/app/models/file.ts +++ b/app/models/file.ts @@ -133,6 +133,16 @@ export default class FileModel extends ModelBaseMixin { @belongsTo('file', { inverse: null, async: true }) declare previousFile: AsyncBelongsTo; + async getLastDynamicScan( + fileId: string, + mode: number, + isScheduledScan = false + ) { + const adapter = this.store.adapterFor('file'); + + return await adapter.getLastDynamicScan(fileId, mode, isScheduledScan); + } + analysesSorting = ['computedRisk:desc']; scanProgressClass(type?: boolean) { diff --git a/app/models/profile.ts b/app/models/profile.ts index 6cad5b2b9..cf82fe9e7 100644 --- a/app/models/profile.ts +++ b/app/models/profile.ts @@ -1,18 +1,6 @@ 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', -} - interface ValueObject { value: boolean; is_inherited: boolean; @@ -37,33 +25,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' @@ -79,65 +40,12 @@ export default class ProfileModel extends Model { @attr('boolean') declare showUnknownAnalysis: boolean; - @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 +66,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..9e55fac53 100644 --- a/app/router.ts +++ b/app/router.ts @@ -203,7 +203,8 @@ Router.map(function () { this.route('dynamic-scan', function () { this.route('manual'); - // this.route('automated'); + this.route('automated'); + this.route('scheduled-automated'); this.route('results'); }); }); diff --git a/app/routes/authenticated/dashboard/file/dynamic-scan/scheduled-automated.ts b/app/routes/authenticated/dashboard/file/dynamic-scan/scheduled-automated.ts new file mode 100644 index 000000000..a6c6474f1 --- /dev/null +++ b/app/routes/authenticated/dashboard/file/dynamic-scan/scheduled-automated.ts @@ -0,0 +1,38 @@ +import { service } from '@ember/service'; +import type Store from '@ember-data/store'; +import type RouterService from '@ember/routing/router-service'; + +import AkBreadcrumbsRoute from 'irene/utils/ak-breadcrumbs-route'; +import { ScrollToTop } from 'irene/utils/scroll-to-top'; +import type DynamicScanService from 'irene/services/dynamic-scan'; + +export default class AuthenticatedDashboardFileDynamicScanScheduledAutomatedRoute extends ScrollToTop( + AkBreadcrumbsRoute +) { + @service declare store: Store; + @service declare router: RouterService; + @service('dynamic-scan') declare dsService: DynamicScanService; + + beforeModel(): Promise | void { + if (!this.dsService.showScheduledScan) { + const { fileid } = this.paramsFor('authenticated.dashboard.file') as { + fileid: string; + }; + + this.router.transitionTo('authenticated.dashboard.file', fileid); + } + } + + async model() { + const { fileid } = this.paramsFor('authenticated.dashboard.file') as { + fileid: string; + }; + + const file = await this.store.findRecord('file', fileid); + + return { + file, + profileId: (await file.project).activeProfileId, + }; + } +} 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/dynamic-scan.ts b/app/services/dynamic-scan.ts new file mode 100644 index 000000000..72d6136ec --- /dev/null +++ b/app/services/dynamic-scan.ts @@ -0,0 +1,150 @@ +import Service, { service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import { action } from '@ember/object'; +import type IntlService from 'ember-intl/services/intl'; +import type RouterService from '@ember/routing/router-service'; + +import type FileModel from 'irene/models/file'; +import type OrganizationService from './organization'; +import DynamicscanModel from 'irene/models/dynamicscan'; +import parseError from 'irene/utils/parse-error'; +import ENUMS from 'irene/enums'; + +export default class DynamicScanService extends Service { + @service declare intl: IntlService; + @service declare router: RouterService; + @service declare organization: OrganizationService; + @service('notifications') declare notify: NotificationService; + + @tracked manualScan: DynamicscanModel | null = null; + @tracked automatedScan: DynamicscanModel | null = null; + @tracked scheduledScan: DynamicscanModel | null = null; + + currentFile: FileModel | null = null; + + get isSuperUserAndAutomationEnabled() { + return ( + this.organization.isSecurityEnabled && + this.organization.selected?.features?.dynamicscan_automation + ); + } + + get showScheduledScan() { + return this.isSuperUserAndAutomationEnabled && this.scheduledScan; + } + + /** + * Fetches the latest scans for a given file, including manual, automated, and scheduled scans. + * This method is called from the `authenticated.dashboard.file.dynamic-scan` root component. + * + * @param file - The file for which to fetch the latest scans. + */ + @action + fetchLatestScans(file: FileModel) { + // save file reference + this.currentFile = file; + + this.fetchLatestManualScan.perform(file); + this.fetchLatestAutomatedScan.perform(file); + + if (this.isSuperUserAndAutomationEnabled) { + this.fetchLatestScheduledScan.perform(file); + } + } + + @action + resetScans() { + this.manualScan = null; + this.automatedScan = null; + this.scheduledScan = null; + this.currentFile = null; + } + + @action + async fetchLatestScan( + file: FileModel, + mode: number, + isScheduledScan = false + ) { + try { + return await file.getLastDynamicScan(file.id, mode, isScheduledScan); + } catch (e) { + this.notify.error(parseError(e, this.intl.t('pleaseTryAgain'))); + + return null; + } + } + + fetchLatestManualScan = task(async (file: FileModel) => { + this.manualScan = await this.fetchLatestScan( + file, + ENUMS.DYNAMIC_MODE.MANUAL + ); + }); + + fetchLatestAutomatedScan = task(async (file: FileModel) => { + this.automatedScan = await this.fetchLatestScan( + file, + ENUMS.DYNAMIC_MODE.AUTOMATED + ); + }); + + fetchLatestScheduledScan = task(async (file: FileModel) => { + this.scheduledScan = await this.fetchLatestScan( + file, + ENUMS.DYNAMIC_MODE.AUTOMATED, + true + ); + }); + + /** + * Updates the current scan status for a file based on the incoming dynamic scan model. + * + * @description + * This task checks if the incoming scan model belongs to the current file and updates + * either the manual or automated scan accordingly. It ensures that only the most recent + * scan for the current file is tracked. + * + * @param model - The dynamic scan model to process + */ + checkScanInProgressAndUpdate = task(async (model: unknown) => { + // Only process if the model is a DynamicscanModel + if (!(model instanceof DynamicscanModel)) { + return; + } + + // Check if we're on the correct route and have a current file + const isOnDynamicScanRoute = this.router.currentRouteName.includes( + 'authenticated.dashboard.file.dynamic-scan' + ); + + if (!this.currentFile || !isOnDynamicScanRoute) { + return; + } + + // Check if the scan belongs to the current file + const isSameFile = model.file.get('id') === this.currentFile.id; + + if (!isSameFile) { + return; + } + + // Update manual scan if conditions are met + if ( + model.mode === ENUMS.DYNAMIC_MODE.MANUAL && + model.id !== this.manualScan?.id + ) { + this.manualScan = model; + return; + } + + // Update automated scan if conditions are met + if ( + model.mode === ENUMS.DYNAMIC_MODE.AUTOMATED && + model.id !== this.automatedScan?.id + ) { + this.automatedScan = model; + } + }); +} diff --git a/app/services/organization.ts b/app/services/organization.ts index a34d79242..0c8814cb3 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'; import type IreneAjaxService from './ajax'; export default class OrganizationService extends Service { @@ -32,9 +33,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()[0]; if (selectedOrg) { this.selected = selectedOrg; @@ -44,7 +45,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/services/websocket.ts b/app/services/websocket.ts index ff7379e6e..d6bc24683 100644 --- a/app/services/websocket.ts +++ b/app/services/websocket.ts @@ -12,6 +12,7 @@ import type RealtimeService from './realtime'; import type AkNotificationsService from './ak-notifications'; import type UserModel from 'irene/models/user'; import type IreneAjaxService from './ajax'; +import type DynamicScanService from './dynamic-scan'; export interface SocketInstance { on: (event: string, handler: (args: any) => void, target?: object) => void; @@ -35,6 +36,7 @@ export default class WebsocketService extends Service { @service declare notifications: NotificationService; @service declare akNotifications: AkNotificationsService; @service declare ajax: IreneAjaxService; + @service('dynamic-scan') declare dsService: DynamicScanService; @service('socket-io') socketIOService: any; connectionPath = '/websocket'; @@ -254,7 +256,10 @@ export default class WebsocketService extends Service { try { this.store.modelFor(modelName); - await this.store.findRecord(modelName, id); + const model = await this.store.findRecord(modelName, id); + + // to check if scan is in progress and update for real time status + this.dsService.checkScanInProgressAndUpdate.perform(model); } catch (error) { this.logger.error(error); } diff --git a/app/styles/_component-variables.scss b/app/styles/_component-variables.scss index 06f06832f..37fe2029b 100644 --- a/app/styles/_component-variables.scss +++ b/app/styles/_component-variables.scss @@ -1941,9 +1941,13 @@ body { // variables for file-details/dynamic-scan/header --file-details-dynamic-scan-header-background-color: var(--background-light); - // variables for file-details/dynamic-scan/manual - --file-details-dynamic-scan-manual-border-color: var(--border-color-1); - --file-details-dynamic-scan-manual-background-main: var(--background-main); + // variables for file-details/dynamic-scan/device-wrapper + --file-details-dynamic-scan-device-wrapper-border-color: var( + --border-color-1 + ); + --file-details-dynamic-scan-device-wrapper-background-main: var( + --background-main + ); // variables for file-details/dynamic-scan/automated --file-details-dynamic-scan-automated-border-color: var(--border-color-1); 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/app/templates/authenticated/dashboard/file/dynamic-scan/scheduled-automated.hbs b/app/templates/authenticated/dashboard/file/dynamic-scan/scheduled-automated.hbs new file mode 100644 index 000000000..7a5acafa4 --- /dev/null +++ b/app/templates/authenticated/dashboard/file/dynamic-scan/scheduled-automated.hbs @@ -0,0 +1,6 @@ +{{page-title 'Scheduled Automated'}} + + \ 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/deprecated-routes/projects-redirect-test.js b/tests/acceptance/deprecated-routes/projects-redirect-test.js index a2309be99..516ec28f4 100644 --- a/tests/acceptance/deprecated-routes/projects-redirect-test.js +++ b/tests/acceptance/deprecated-routes/projects-redirect-test.js @@ -73,8 +73,8 @@ module('Acceptance | projects redirect', function (hooks) { }; }); - this.server.create('device-preference', { - id: profile.id, + this.server.get('v2/profiles/:id/automation_preference', (_, req) => { + return { id: req.params.id, dynamic_scan_automation_enabled: true }; }); this.server.create('proxy-setting', { id: profile.id }); @@ -90,15 +90,14 @@ module('Acceptance | projects redirect', 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('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - - return { count: results.length, next: null, previous: null, results }; - }); + this.server.get( + '/v2/profiles/:id/ds_automated_device_preference', + (schema, req) => { + return schema.dsAutomatedDevicePreferences + .find(`${req.params.id}`) + ?.toJSON(); + } + ); this.server.get('/profiles/:id/proxy_settings', (schema, req) => { return schema.proxySettings.find(`${req.params.id}`)?.toJSON(); diff --git a/tests/acceptance/file-compare-test.js b/tests/acceptance/file-compare-test.js index 99f30bfdb..d58c69a31 100644 --- a/tests/acceptance/file-compare-test.js +++ b/tests/acceptance/file-compare-test.js @@ -153,7 +153,7 @@ module('Acceptance | file compare', function (hooks) { await visit(`/dashboard/project/${this.fileOld?.id}/files`); - const profile = this.server.create('profile', { id: '1' }); + this.server.create('profile', { id: '1' }); this.server.get('/profiles/:id/proxy_settings', (_, req) => { return { @@ -194,10 +194,6 @@ module('Acceptance | file compare', function (hooks) { return { api_url_filters: '', id: req.params.id }; }); - this.server.create('device-preference', { - id: profile.id, - }); - this.server.get('/organizations/:id/jira_projects', () => { return { count: 0, next: null, previous: null, results: [] }; }); diff --git a/tests/acceptance/file-details-test.js b/tests/acceptance/file-details-test.js index 3c534609f..258e4aa4d 100644 --- a/tests/acceptance/file-details-test.js +++ b/tests/acceptance/file-details-test.js @@ -78,12 +78,6 @@ module('Acceptance | file details', function (hooks) { status: true, }); - this.server.create('dynamicscan-old', { expires_on: null }); - - this.server.create('device-preference', { - id: profile.id, - }); - // Server mocks this.server.get('/organizations/:id', (schema, req) => schema.organizationMes.find(`${req.params.id}`)?.toJSON() @@ -114,8 +108,22 @@ module('Acceptance | file details', function (hooks) { }; }); - this.server.get('/dynamicscan/:id', (schema, req) => { - return schema.dynamicscanOlds.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/device_preference', (schema, req) => { @@ -268,6 +276,14 @@ module('Acceptance | file details', function (hooks) { }); test('test api view details click to navigate to api scan page', async function (assert) { + this.server.get('/v2/files/:id/capturedapis', (schema, req) => { + const results = req.queryParams.is_active + ? schema.db.capturedapis.where({ is_active: true }) + : schema.capturedapis.all().models; + + return { count: results.length, previous: null, next: null, results }; + }); + await visit('/dashboard/file/1'); await click('[data-test-fileDetailScanActions-apiScanViewDetails]'); diff --git a/tests/acceptance/file-details/dynamic-scan-test.js b/tests/acceptance/file-details/dynamic-scan-test.js index e8522911b..12fceba85 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,113 @@ 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'], -}; +const commondynamicScanStatusTextList = [ + { + 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.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'), + }, +]; + +const manualDynamicScanStatusTextList = [ + { + status: ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION, + text: () => t('running'), + statusHidden: true, + }, +]; + +const autoDynamicScanStatusTextList = [ + { + status: ENUMS.DYNAMIC_SCAN_STATUS.INITIATING_AUTO_INTERACTION, + text: () => t('running'), + }, + { + status: ENUMS.DYNAMIC_SCAN_STATUS.AUTO_INTERACTION_COMPLETED, + text: () => t('running'), + }, +]; class IntegrationStub extends Service { async configure(user) { @@ -34,26 +136,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 +162,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 +232,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 +266,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,651 +296,754 @@ 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); - }); + test.each( + 'it renders DAST page with different states of scan status', + [ + { + route: 'manual', + mode: ENUMS.DYNAMIC_MODE.MANUAL, + statusTextList: [ + ...commondynamicScanStatusTextList, + ...manualDynamicScanStatusTextList, + ], + }, + { + route: 'automated', + mode: ENUMS.DYNAMIC_MODE.AUTOMATED, + statusTextList: [ + ...commondynamicScanStatusTextList, + ...autoDynamicScanStatusTextList, + ], + }, + ].reduce((acc, { route, mode, statusTextList }) => { + return [ + ...acc, + ...statusTextList.map((it) => ({ + mode, + route, + ...it, + })), + ]; + }, []), + async function (assert, { mode, route, status, text, statusHidden }) { + const { id } = this.server.create('dynamicscan', { + file: this.file.id, + mode, + status, + ended_on: null, + }); - assert.dom('[data-test-fileDetailsSummary-root]').exists(); + const isManualMode = mode === ENUMS.DYNAMIC_MODE.MANUAL; - const tabs = [ - { id: 'manual-dast-tab', label: 'dastTabs.manualDAST' }, - // { id: 'automated-dast-tab', label: 'dastTabs.automatedDAST' }, - { id: 'dast-results-tab', label: 'dastTabs.dastResults' }, - ]; + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/${route}`); - 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(); + this.breadcrumbsService.breadcrumbItems.map((item) => { + assert + .dom(`[data-test-ak-breadcrumbs-auto-trail-item-key="${item.route}"]`) + .exists() + .containsText(item.title); + }); - await click( - '[data-test-fileDetails-dynamicScan-manualDastFullScreen-closeBtn]' - ); + assert.dom('[data-test-fileDetailsSummary-root]').exists(); - assert - .dom('[data-test-vncViewer-root]') - .exists() - .doesNotHaveClass(/vnc-viewer-fullscreen/); + 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-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, - }); + tabs.map((item) => { + assert + .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) + .exists(); - this.server.get('/v2/dynamicscans/:id', (schema, req) => { - return schema.dynamicscans.find(`${req.params.id}`)?.toJSON(); - }); + assert + .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) + .containsText(t(item.label)); + }); - await visit(`/dashboard/file/${this.file.id}/dynamic-scan/manual`); + assert + .dom('[data-test-fileDetails-dynamicScan-deviceWrapper-headerText]') + .exists() + .hasText(t('realDevice')); - assert - .dom('[data-test-fileDetails-dynamicScan-header-breadcrumbContainer]') - .exists(); + const dynamicscan = this.store.peekRecord('dynamicscan', id); - const breadcrumbItems = [t('allProjects'), t('scanDetails'), t('dast')]; + assert.strictEqual( + dynamicscan.statusText, + status !== ENUMS.DYNAMIC_SCAN_STATUS.NOT_STARTED ? text() : t('unknown') + ); - breadcrumbItems.map((item) => { - assert - .dom( - `[data-test-fileDetails-dynamicScan-header-breadcrumbItem="${item}"]` - ) - .exists(); - }); + if (!statusHidden) { + assert + .dom('[data-test-fileDetails-dynamicScan-statusChip]') + .hasText(text()); + } else { + assert + .dom('[data-test-fileDetails-dynamicScan-statusChip]') + .doesNotExist(); + } - assert.dom('[data-test-fileDetailsSummary-root]').exists(); + if (dynamicscan.isShuttingDown) { + assert.dom(`[data-test-fileDetails-dynamicScanAction]`).doesNotExist(); + } else { + if (dynamicscan.isStarting) { + assert + .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( + isManualMode ? t('dynamicScan') : t('dastTabs.automatedDAST') + ); + } else { + assert + .dom(`[data-test-fileDetails-dynamicScanAction="startBtn"]`) + .isNotDisabled() + .hasText( + isManualMode ? t('dynamicScan') : t('dastTabs.automatedDAST') + ); + } + } - const tabs = [ - { id: 'manual-dast-tab', label: 'dastTabs.manualDAST' }, - // { id: 'automated-dast-tab', label: 'dastTabs.automatedDAST' }, - { id: 'dast-results-tab', label: 'dastTabs.dastResults' }, - ]; + if (dynamicscan.isReady) { + assert + .dom( + '[data-test-fileDetails-dynamicScan-deviceWrapper-deviceViewer-fullscreenBtn]' + ) + .isNotDisabled(); + } else { + assert + .dom( + '[data-test-fileDetails-dynamicScan-deviceWrapper-deviceViewer-fullscreenBtn]' + ) + .doesNotExist(); + } - tabs.map((item) => { assert - .dom(`[data-test-fileDetails-dynamicScan-header="${item.id}"]`) + .dom('[data-test-fileDetails-dynamicScan-deviceWrapper-deviceViewer]') .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(); + if (dynamicscan.isReady) { + assert.dom('[data-test-NovncRfb-canvasContainer]').exists(); + } else { + assert.dom('[data-test-NovncRfb-canvasContainer]').doesNotExist(); + } - assert.dom('[data-test-fileDetailsSummary-root]').exists(); + if (isManualMode) { + assert.dom('[data-test-vncViewer-automatedNote]').doesNotExist(); + assert.dom('[data-test-vncViewer-scanTriggeredNote]').doesNotExist(); + + if (dynamicscan.isStarting) { + // Check manual note + assert + .dom('[data-test-vncViewer-manualScanNote]') + .exists() + .hasText(`${t('note')} - ${t('dynamicScanText')}`); + } else if (dynamicscan.isReadyOrRunning) { + assert.dom('[data-test-vncViewer-manualScanNote]').doesNotExist(); + assert.dom('[data-test-fileDetails-dynamicScan-expiry]').exists(); + } + } else { + if ( + dynamicscan.isStartingOrShuttingInProgress || + dynamicscan.isReadyOrRunning + ) { + assert.dom('[data-test-vncViewer-manualScanNote]').doesNotExist(); + + assert + .dom('[data-test-fileDetails-dynamicScan-expiry]') + .doesNotExist(); + + // Check automated note + assert.dom('[data-test-vncViewer-automatedNote]').exists(); + + assert + .dom('[data-test-vncViewer-scanTriggeredAutomaticallyText]') + .exists() + .hasText(t('scanTriggeredAutomatically')); + + if (dynamicscan.isInqueue) { + assert + .dom('[data-test-vncViewer-automatedNote]') + .hasText(`${t('note')} - ${t('automatedScanQueuedVncNote')}`); + } else { + assert + .dom('[data-test-vncViewer-automatedNote]') + .hasText(`${t('note')} - ${t('automatedScanRunningVncNote')}`); + } + } + } + } + ); - assert.dom('[data-test-fileDetails-dynamicScan-expiry]').exists(); + test.each( + 'it should start & stop/cancel dynamic scan', + [ + { + mode: 'manual', + cancelInBetween: false, + expectedAssertions: 41, + startedBy: false, + }, + { + mode: 'manual', + cancelInBetween: true, + expectedAssertions: 35, + startedBy: false, + }, + { + mode: 'automated', + cancelInBetween: false, + expectedAssertions: 48, + startedBy: true, + }, + { + mode: 'automated', + cancelInBetween: true, + expectedAssertions: 41, + startedBy: false, + }, + ], + async function ( + assert, + { mode, cancelInBetween, expectedAssertions, startedBy } + ) { + 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, + started_by_user: startedBy ? this.profile.id : 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 = JSON.parse(req.requestBody); - assert - .dom(`[data-test-vncViewer-automatedNote]`) - .exists() - .containsText(t('automatedScanVncNote')); + // assert request body + assert.strictEqual( + reqBody.mode, + ENUMS.DYNAMIC_MODE[mode.toUpperCase()] + ); - assert - .dom('[data-test-fileDetails-dynamicScan-automatedDast-fullscreenBtn]') - .exists(); + assert.false(reqBody.enable_api_capture); - 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: '' }; + const user = this.server.create('user', { + id: this.profile.id, }); - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; - }); + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/${mode}`); - 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-fileDetails-dynamicScan-header-breadcrumbContainer]') + .exists(); - this.server.post('/v2/files/:id/dynamicscans', (_, req) => { - const reqBody = objectifyEncodedReqBody(req.requestBody); + assert.dom('[data-test-fileDetailsSummary-root]').exists(); - assert.strictEqual(reqBody.mode, scanTypeText); + assert + .dom('[data-test-fileDetails-dynamicScan-deviceWrapper-headerText]') + .hasText(t('realDevice')); - assert.strictEqual(reqBody.enable_api_capture, 'false'); + assert + .dom('[data-test-fileDetails-dynamicScanAction="startBtn"]') + .isNotDisabled() + .hasText( + mode === 'manual' ? t('dynamicScan') : t('dastTabs.automatedDAST') + ); - // 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-deviceWrapper-deviceViewer-fullscreenBtn]' + ) + .doesNotExist(); - return new Response(200); - }); + assert + .dom('[data-test-fileDetails-dynamicScan-deviceWrapper-deviceViewer]') + .exists(); - await visit( - `/dashboard/file/${this.file.id}/dynamic-scan/${scanTypeText.toLowerCase()}` - ); + assert.dom('[data-test-NovncRfb-canvasContainer]').doesNotExist(); - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .exists() - .containsText(`${scanTypeText} DAST`); + // Click start button + await click('[data-test-fileDetails-dynamicScanAction="startBtn"]'); - // Load dynamic scan drawer - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); + assert + .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(); + const assertScanStatus = createDynamicScanStatusHelper( + this.owner, + this.dynamicscan + ); - this.file = this.store.push( - this.store.normalize('file', this.file.toJSON()) + // Test the scan status flow + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.IN_QUEUE, + t('deviceInQueue'), + 'cancelBtn' ); - const DYNAMIC_SCAN_MODEL_ID = this.profile.id; - const scanTypeText = isAutomated ? 'Automated' : 'Manual'; + if (mode === 'automated') { + assert.dom('[data-test-vncViewer-manualScanNote]').doesNotExist(); - 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, - }); + assert.dom('[data-test-vncViewer-scanTriggeredNote]').exists(); - this.server.get('/profiles/:id/api_scan_options', (schema, req) => { - return { id: req.params.id, api_url_filters: '' }; - }); + if (startedBy) { + assert + .dom('[data-test-vncViewer-scanStartedByText]') + .exists() + .hasText(`${t('scanStartedBy')} ${user.username}`); + } else { + assert + .dom('[data-test-vncViewer-scanTriggeredAutomaticallyText]') + .exists() + .hasText(t('scanTriggeredAutomatically')); + } - this.server.delete('/dynamicscans/:id', (_, req) => { - // It deletes the correct dynamic scan ID - assert.strictEqual(dynamicScan.id, req.params.id); + assert + .dom('[data-test-vncViewer-automatedNote]') + .hasText(`${t('note')} - ${t('automatedScanQueuedVncNote')}`); - // Start automated scan for dynamic scan object - this.server.db.dynamicscans.update(DYNAMIC_SCAN_MODEL_ID, { - status: ENUMS.DYNAMIC_STATUS.NONE, - }); + assert + .dom('[data-test-fileDetails-dynamicScan-inProgress-loader]') + .exists(); + } else { + assert.dom('[data-test-vncViewer-automatedNote]').doesNotExist(); + assert.dom('[data-test-vncViewer-scanTriggeredNote]').doesNotExist(); - return new Response(204); - }); + assert + .dom('[data-test-vncViewer-manualScanNote]') + .hasText(`${t('note')} - ${t('dynamicScanText')}`); + } + + assert.dom('[data-test-fileDetails-dynamicScan-expiry]').doesNotExist(); - await visit( - `/dashboard/file/${this.file.id}/dynamic-scan/${scanTypeText.toLowerCase()}` + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.DEVICE_ALLOCATED, + t('deviceBooting'), + '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]'; + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.INSTALLING, + t('deviceInstalling'), + 'cancelBtn' + ); - assert.dom(scanStartBtn).doesNotExist(); + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.LAUNCHING, + t('deviceLaunching'), + 'cancelBtn' + ); - if (!isAutomated) { - assert.dom(statusChipSelector).doesNotExist(); + 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' + ); - assert.dom(stopBtn).exists().containsText('Stop'); + 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' + ); - await click(stopBtn); + if (mode === 'automated') { + assert.dom('[data-test-vncViewer-scanTriggeredNote]').exists(); - assert.dom(statusChipSelector).exists().containsText('Stopping'); + assert + .dom('[data-test-vncViewer-automatedNote]') + .hasText(`${t('note')} - ${t('automatedScanRunningVncNote')}`); + } else { + assert.dom('[data-test-fileDetails-dynamicScan-expiry]').exists(); + } - 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-manualScanNote]').doesNotExist(); - assert - .dom('[data-test-fileDetails-dynamicScan-expiry-time]') - .exists() - .hasText('00:00'); + // Stop scan + await click('[data-test-fileDetails-dynamicScanAction="stopBtn"]'); + } - assert - .dom('[data-test-vncViewer-scanTriggeredAutomaticallyText]') - .exists() - .hasText('Scan triggered automatically on app upload.'); + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.SHUTTING_DOWN, + t('deviceShuttingDown') + ); - await click(cancelBtn); + assert.dom('[data-test-fileDetails-dynamicScanAction]').doesNotExist(); - assert.dom(statusChipSelector).hasText('Stopping'); - assert.dom(cancelBtn).doesNotExist(); + 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(scanStartBtn).exists().containsText(`${scanTypeText} DAST`); - } + 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.skip('it should render toggle dast ui if automated dast is not enabled', async function (assert) { - this.dynamicscanMode.update({ dynamicscan_mode: 'Manual' }); + test('dynamic scan scheduled automated flow', async function (assert) { + assert.expect(); - this.server.create('proxy-setting', { id: this.profile.id }); + const createDynamicscan = () => + this.server.create('dynamicscan', { + file: this.file.id, + mode: ENUMS.DYNAMIC_MODE.AUTOMATED, + status: ENUMS.DYNAMIC_SCAN_STATUS.NOT_STARTED, + ended_on: null, + }); - this.server.get('/profiles/:id/proxy_settings', (schema, req) => { - return schema.proxySettings.find(`${req.params.id}`)?.toJSON(); + this.server.create('ds-automated-device-preference', { + id: this.profile.id, }); - this.server.get('/profiles/:id/api_scan_options', (_, req) => ({ - id: req.params.id, - api_url_filters: '', - })); + this.server.post('/v2/files/:id/dynamicscans', (schema, req) => { + const reqBody = JSON.parse(req.requestBody); - 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, - }; - } - ); + // assert request body + assert.strictEqual(reqBody.mode, ENUMS.DYNAMIC_MODE.AUTOMATED); - 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, - }; - } - ); + assert.false(reqBody.enable_api_capture); - this.server.get( - '/organizations/:id/projects/:projectId/collaborators', - (schema) => { - const results = schema.projectCollaborators.all().models; + // create and set dynamicscan data reference + this.dynamicscan = createDynamicscan(); - return { count: results.length, next: null, previous: null, results }; - } - ); + return this.dynamicscan.toJSON(); + }); - this.server.get( - '/organizations/:orgId/projects/:projectId/teams', - (schema) => { - const results = schema.projectTeams.all().models; + this.server.get('/v2/dynamicscans/:id', (schema, req) => { + return schema.dynamicscans.find(`${req.params.id}`)?.toJSON(); + }); - return { count: results.length, next: null, previous: null, results }; - } - ); + this.server.delete('/v2/dynamicscans/:id', () => { + this.dynamicscan.update({ + status: ENUMS.DYNAMIC_SCAN_STATUS.STOP_SCAN_REQUESTED, + }); - this.server.get( - '/organizations/:id/github_repos', - () => new Response(404, {}, { detail: 'Github not integrated' }) - ); + return new Response(204); + }); this.server.get( - '/projects/:id/github', - () => new Response(400, {}, { detail: 'Github not integrated' }) + '/v2/profiles/:id/ds_automated_device_preference', + (schema, req) => { + return schema.dsAutomatedDevicePreferences + .find(`${req.params.id}`) + ?.toJSON(); + } ); - this.server.get( - '/organizations/:id/jira_projects', - () => new Response(404, {}, { detail: 'JIRA not integrated' }) - ); + this.server.get('/profiles/:id/api_scan_options', () => ({ + id: '1', + api_url_filters: '', + })); - this.server.get( - '/projects/:id/jira', - () => new Response(404, {}, { detail: 'JIRA not integrated' }) - ); + this.server.get('/profiles/:id/proxy_settings', () => ({ + id: '1', + host: faker.internet.ip(), + port: faker.internet.port(), + enabled: false, + })); - this.server.get('/profiles/:id/dynamicscan_mode', (schema, req) => { - return schema.dynamicscanModes.find(`${req.params.id}`).toJSON(); + this.server.get('/profiles/:id/unknown_analysis_status', (_, req) => { + return { + id: req.params.id, + status: false, + }; }); await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); assert - .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledCard]') - .exists(); + .dom('[data-test-fileDetails-dynamicScan-deviceWrapper-headerText]') + .hasText(t('realDevice')); assert - .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledTitle]') - .exists() - .containsText(t('toggleAutomatedDAST')); + .dom('[data-test-fileDetails-dynamicScanAction="startBtn"]') + .isNotDisabled() + .hasText(t('dastTabs.automatedDAST')); + + assert.dom('[data-test-NovncRfb-canvasContainer]').doesNotExist(); + + assert + .dom( + '[data-test-fileDetails-dynamicScan-header="scheduled-automated-dast-tab"]' + ) + .doesNotExist(); + + // Click start button + await click('[data-test-fileDetails-dynamicScanAction="startBtn"]'); - // TODO: add containsText here after correct text is given assert - .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledDesc]') + .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer]') .exists(); + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer-title]') + .hasText(t('dastTabs.automatedDAST')); + + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') + .isNotDisabled() + .hasText(t('modalCard.dynamicScan.restartScan')); + + // start dynamic scan + await click('[data-test-fileDetails-dynamicScanDrawer-startBtn]'); + + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer]') + .doesNotExist(); + + const assertScanStatus = createDynamicScanStatusHelper( + this.owner, + this.dynamicscan + ); + + // Test the scan status flow + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.IN_QUEUE, + t('deviceInQueue'), + 'cancelBtn' + ); + + assert.dom('[data-test-vncViewer-manualScanNote]').doesNotExist(); + + assert.dom('[data-test-vncViewer-scanTriggeredNote]').exists(); + + assert + .dom('[data-test-vncViewer-automatedNote]') + .hasText(`${t('note')} - ${t('automatedScanQueuedVncNote')}`); + + // simulate running state + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.DEVICE_ALLOCATED, + t('deviceBooting'), + 'cancelBtn' + ); + + assert + .dom('[data-test-vncViewer-automatedNote]') + .hasText(`${t('note')} - ${t('automatedScanRunningVncNote')}`); + + // go back and come to simulate refresh + await visit(`/dashboard/file/${this.file.id}`); + await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); + assert .dom( - '[data-test-fileDetails-dynamicScan-automatedDast-disabledActionBtn]' + '[data-test-fileDetails-dynamicScan-header="scheduled-automated-dast-tab"] a' ) - .exists(); + .hasText(t('dastTabs.scheduledAutomatedDAST')); + // navigate to scheduled tab await click( - '[data-test-fileDetails-dynamicScan-automatedDast-disabledActionBtn]' + '[data-test-fileDetails-dynamicScan-header="scheduled-automated-dast-tab"] a' ); assert.strictEqual( currentURL(), - `/dashboard/project/${this.file.id}/settings` + `/dashboard/file/${this.file.id}/dynamic-scan/scheduled-automated` ); - }); - test.skip('it should render upselling ui if automated dast is not enabled', async function (assert) { - this.file.update({ can_run_automated_dynamicscan: false }); + assert + .dom('[data-test-fileDetails-dynamicScan-deviceWrapper-headerText]') + .hasText(t('realDevice')); - this.organization.update({ - features: { - dynamicscan_automation: false, - }, - }); + assert + .dom('[data-test-fileDetails-dynamicScanAction="startBtn"]') + .doesNotExist(); - await visit(`/dashboard/file/${this.file.id}/dynamic-scan/automated`); + assert + .dom('[data-test-fileDetails-dynamicScan-statusChip]') + .hasText(t('deviceBooting')); + + assert + .dom(`[data-test-fileDetails-dynamicScanAction="cancelBtn"]`) + .isNotDisabled(); + + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION, + null, + 'stopBtn' + ); + + // screen canvas + assert.dom('[data-test-NovncRfb-canvasContainer]').exists(); + + // Stop scan + await click('[data-test-fileDetails-dynamicScanAction="stopBtn"]'); + + // stops and redirects to automated tab + assert.strictEqual( + currentURL(), + `/dashboard/file/${this.file.id}/dynamic-scan/automated` + ); - assert.dom('[data-test-automated-dast-upselling]').exists(); + assert + .dom( + '[data-test-fileDetails-dynamicScan-header="scheduled-automated-dast-tab"]' + ) + .doesNotExist(); + + await assertScanStatus( + assert, + ENUMS.DYNAMIC_SCAN_STATUS.SHUTTING_DOWN, + t('deviceShuttingDown') + ); + + assert.dom('[data-test-fileDetails-dynamicScanAction]').doesNotExist(); }); test('it should navigate properly on tab click', async function (assert) { @@ -833,18 +1057,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/acceptance/project-settings/dynamicscan-automation-settings-test.js b/tests/acceptance/project-settings/dynamicscan-automation-settings-test.js index 3a8e4368b..accf6f021 100644 --- a/tests/acceptance/project-settings/dynamicscan-automation-settings-test.js +++ b/tests/acceptance/project-settings/dynamicscan-automation-settings-test.js @@ -98,13 +98,14 @@ module( project, }); - this.server.create('device-preference', { + this.server.create('ds-automated-device-preference', { id: profile.id, }); this.server.create('proxy-setting', { id: profile.id }); - this.server.create('dynamicscan-mode', { id: profile.id }); + this.server.createList('available-automated-device', 3); + // Register Services this.owner.register('service:integration', IntegrationStub); this.owner.register('service:websocket', WebsocketStub); @@ -123,20 +124,61 @@ module( schema.profiles.find(`${req.params.id}`)?.toJSON() ); - this.server.get('/dynamicscan/:id', (schema, req) => { - return schema.dynamicscanOlds.find(`${req.params.id}`)?.toJSON(); + this.server.get('v2/profiles/:id/automation_preference', (_, req) => { + return { id: req.params.id, dynamic_scan_automation_enabled: true }; }); - 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('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; + this.server.get( + '/v2/profiles/:id/ds_manual_device_preference', + (schema, req) => { + return schema.dsManualDevicePreferences + .find(`${req.params.id}`) + ?.toJSON(); + } + ); + + this.server.get( + '/v2/profiles/:id/ds_automated_device_preference', + (schema, req) => { + return schema.dsAutomatedDevicePreferences + .find(`${req.params.id}`) + ?.toJSON(); + } + ); + + this.server.get('/v2/projects/:id/available_manual_devices', (schema) => { + const results = schema.availableManualDevices.all().models; return { count: results.length, next: null, previous: null, results }; }); + this.server.get( + '/v2/projects/:id/available_automated_devices', + (schema) => { + const results = schema.availableAutomatedDevices.all().models; + + return { count: results.length, next: null, previous: null, results }; + } + ); + this.server.get('/profiles/:id/proxy_settings', (schema, req) => { return schema.proxySettings.find(`${req.params.id}`)?.toJSON(); }); @@ -164,10 +206,6 @@ module( } ); - this.server.get('/profiles/:id/dynamicscan_mode', (schema, req) => { - return schema.dynamicscanModes.find(req.params.id).toJSON(); - }); - this.server.get('/v2/scan_parameter_groups/:id', (schema, req) => schema.scanParameterGroups.find(req.params.id).toJSON() ); 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 deleted file mode 100644 index ddb0bca0c..000000000 --- a/tests/integration/components/file-details/dynamic-scan/action/drawer-test.js +++ /dev/null @@ -1,1342 +0,0 @@ -import { - click, - fillIn, - find, - findAll, - 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 { dsManualDevicePref } from 'irene/helpers/ds-manual-device-pref'; -import { dsAutomatedDevicePref } from 'irene/helpers/ds-automated-device-pref'; -import { deviceType } from 'irene/helpers/device-type'; -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'], -}; - -// 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; - } - - 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; - } -} - -module( - 'Integration | Component | file-details/dynamic-scan/action/drawer', - function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - setupIntl(hooks, 'en'); - - hooks.beforeEach(async function () { - this.server.createList('organization', 1); - - 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, - }); - - const project = this.server.create('project', { file: file.id, id: '1' }); - - // Project Available Devices - const getDeviceCapabilities = () => ({ - has_sim: faker.datatype.boolean(), - has_vpn: faker.datatype.boolean(), - has_pin_lock: faker.datatype.boolean(), - device_identifier: faker.string.alphanumeric(7).toUpperCase(), - platform_version: faker.helpers.arrayElement(['13', '12', '14']), - }); - - const availableDevices = [ - ...this.server.createList('project-available-device', 1, { - is_tablet: true, - platform: 0, - ...getDeviceCapabilities(), - }), - ...this.server.createList('project-available-device', 1, { - is_tablet: true, - platform: 1, - ...getDeviceCapabilities(), - }), - ...this.server.createList('project-available-device', 1, { - is_tablet: false, - platform: 0, - ...getDeviceCapabilities(), - }), - ...this.server.createList('project-available-device', 1, { - is_tablet: false, - platform: 1, - ...getDeviceCapabilities(), - }), - ]; - - 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('v2/profiles/:id/ds_automated_device_preference', () => { - return {}; - }); - - this.server.get('v2/profiles/:id/ds_manual_device_preference', () => { - return {}; - }); - - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; - }); - - const devicePreference = this.server.create('device-preference', { - id: profile.id, - device_type: ENUMS.DEVICE_TYPE.TABLET_REQUIRED, - }); - - this.setProperties({ - file: store.push(store.normalize('file', file.toJSON())), - project: store.push(store.normalize('project', project.toJSON())), - profile: store.push(store.normalize('profile', profile.toJSON())), - onClose: () => {}, - devicePreference, - availableDevices, - store, - }); - - await this.owner.lookup('service:organization').load(); - this.owner.register('service:notifications', NotificationsStub); - 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(); - - const DEFAULT_CHECKED_DEVICE_IDX = 0; - const DEVICE_IDX_TO_SELECT = 1; - - const defaultSelectedDeviceId = - this.availableDevices[DEFAULT_CHECKED_DEVICE_IDX].device_identifier; - - this.server.put( - 'v2/profiles/:id/ds_manual_device_preference', - (_, req) => { - const { ds_manual_device_identifier } = JSON.parse(req.requestBody); - - if (ds_manual_device_identifier) { - const deviceId = - this.availableDevices[DEVICE_IDX_TO_SELECT].device_identifier; - - // eslint-disable-next-line qunit/no-conditional-assertions - assert.strictEqual(deviceId, ds_manual_device_identifier); - } - - return { - ds_manual_device_selection: - ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, - ds_manual_device_identifier: ds_manual_device_identifier, - }; - } - ); - - 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: defaultSelectedDeviceId, - }; - }); - - 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 }; - }); - - await render(hbs` - - - - `); - - 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])) - ); - } - - const anyDeviceLabel = t( - dsManualDevicePref([ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE]) - ); - - // Select "Any Device" - await selectChoose(`.${classes.trigger}`, anyDeviceLabel); - - await click(`.${classes.trigger}`); - selectListItems = findAll('.ember-power-select-option'); - - // "Any Device" is first option - assert.dom(selectListItems[0]).hasAria('selected', 'true'); - - // Select 'Specific Device' - const specificDeviceLabel = t( - dsManualDevicePref([ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE]) - ); - - await selectChoose(`.${classes.trigger}`, specificDeviceLabel); - - await click(`.${classes.trigger}`); - - // "Specific Device" is second option - selectListItems = findAll('.ember-power-select-option'); - - assert.dom(selectListItems[1]).hasAria('selected', 'true'); - - await click(`.${classes.trigger}`); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-root]' - ) - .exists(); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTableHeaderTitle]' - ) - .exists() - .containsText(t('modalCard.dynamicScan.selectSpecificDevice')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-filterSelect]' - ) - .exists(); - - // Sanity check for table items - const deviceElemList = findAll( - '[data-test-fileDetails-dynamicScanDrawer-devicePrefTable-row]' - ); - - assert.strictEqual(deviceElemList.length, this.availableDevices.length); - - const deviceCapabilitiesMap = { - has_sim: 'sim', - has_vpn: 'vpn', - has_pin_lock: 'pinLock', - has_vnc: 'vnc', - }; - - const deviceSelectRadioElement = - '[data-test-fileDetails-dynamicScanDrawer-devicePrefTable-deviceSelectRadioInput]'; - - for (let idx = 0; idx < deviceElemList.length; idx++) { - const deviceElem = deviceElemList[idx]; - const deviceModel = this.availableDevices[idx]; - - const deviceTypeLabel = deviceType([ - deviceModel.is_tablet - ? ENUMS.DEVICE_TYPE.TABLET_REQUIRED - : ENUMS.DEVICE_TYPE.PHONE_REQUIRED, - ]); - - assert.dom(deviceElem).exists(); - - assert.dom(deviceElem).containsText(`${deviceModel.device_identifier}`); - - assert.dom(deviceElem).containsText(t(deviceTypeLabel)); - - assert - .dom(deviceElem) - .exists() - .containsText(`${deviceModel.device_identifier}`); - - // Check for device capability list - Object.keys(deviceCapabilitiesMap).forEach((key) => { - if (deviceModel[key]) { - const capabilityLabel = deviceCapabilitiesMap[key]; - - assert - .dom( - `[data-test-fileDetails-dynamicScanDrawer-devicePrefTable-capabilityId='${capabilityLabel}']`, - deviceElem - ) - .containsText(t(capabilityLabel)); - } - }); - - // Check default selected - if (defaultSelectedDeviceId === deviceModel.device_identifier) { - assert.dom(deviceSelectRadioElement, deviceElem).isChecked(); - } else { - assert.dom(deviceSelectRadioElement, deviceElem).isNotChecked(); - } - - if (DEVICE_IDX_TO_SELECT === idx) { - // Check a selected device - await click(deviceElem.querySelector(deviceSelectRadioElement)); - } - } - - // Check that the select device idx is checked - const checkedElem = deviceElemList[DEVICE_IDX_TO_SELECT]; - - assert.dom(deviceSelectRadioElement, checkedElem).isChecked(); - }); - - test.each( - 'automated DAST: it renders dynamic scan modal', - [{ showProxyPreference: true }, { showProxyPreference: false }], - async function (assert, { showProxyPreference }) { - assert.expect(); - - const dsAutomatedDevicePreference = { - ds_automated_device_selection: faker.helpers.arrayElement([0, 1]), - ds_automated_platform_version_min: faker.number.int({ max: 9 }), - }; - - this.server.get( - 'v2/profiles/:id/ds_automated_device_preference', - () => { - return dsAutomatedDevicePreference; - } - ); - - 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: showProxyPreference, - }; - }); - - await render(hbs` - - - - `); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-title]' - ) - .exists() - .hasText(t('dastTabs.automatedDAST')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-closeBtn]' - ) - .exists(); - - // CTA Buttons - assert - .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') - .exists() - .hasText(t('modalCard.dynamicScan.restartScan')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-settingsPageRedirectBtn]' - ) - .exists() - .hasText(t('modalCard.dynamicScan.goToGeneralSettings')) - .hasAttribute('target', '_blank') - .hasAttribute( - 'href', - `/dashboard/project/${this.file.project.id}/settings` - ); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-headerDeviceRequirements]' - ) - .exists() - .hasText(t('modalCard.dynamicScan.deviceRequirements')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-headerOSInfoDesc]' - ) - .exists() - .containsText(t('modalCard.dynamicScan.osVersion')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-headerOSInfoValue]' - ) - .exists() - .containsText(this.file.project.get('platformDisplay')) - .containsText(t('modalCard.dynamicScan.orAbove')) - .containsText(this.file.minOsVersion); - - // Device Preferences - const devicePrefProps = [ - { - id: 'selectedPref', - title: t('modalCard.dynamicScan.selectedPref'), - value: t( - dsAutomatedDevicePref([ - dsAutomatedDevicePreference.ds_automated_device_selection || - ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA, - ]) - ), - }, - { - id: 'minOSVersion', - title: t('minOSVersion'), - value: - dsAutomatedDevicePreference.ds_automated_platform_version_min, - }, - ]; - - devicePrefProps.forEach((pref) => { - assert - .dom( - `[data-test-fileDetails-dynamicScanDrawer-automatedDast-devicePreference='${pref.id}']` - ) - .exists() - .containsText(String(pref.value)) - .containsText(pref.title); - }); - - // Proxy settings - const proxySetting = this.store.peekRecord( - 'proxy-setting', - this.file.profile.get('id') - ); - - if (proxySetting.enabled) { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsHeader]' - ) - .exists() - .containsText(`${t('enable')} ${t('proxySettingsTitle')}`); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsEnabledChip]' - ) - .exists() - .hasText(t('enabled')); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsRoutingInfo]' - ) - .exists() - .containsText(t('modalCard.dynamicScan.apiRoutingText')) - .containsText(proxySetting.host); - } else { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsContainer]' - ) - .doesNotExist(); - } - } - ); - - test.each( - 'automated DAST: api url filters', - [{ empty: false }, { empty: true }], - async function (assert, { empty }) { - assert.expect(); - - const URL_FILTERS = empty ? '' : 'testurl1.com,testurl2.com'; - - const apiScanOptions = this.store.push( - this.store.normalize( - 'api-scan-options', - this.server - .create('api-scan-options', { api_url_filters: URL_FILTERS }) - .toJSON() - ) - ); - - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); - - this.server.get('/profiles/:id/api_scan_options', (schema, req) => { - return { id: req.params.id, api_url_filters: URL_FILTERS }; - }); - - 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` - - - - `); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-modalBodyWrapper]' - ) - .exists(); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiFilter-title]' - ) - .exists() - .hasText(t('templates.apiScanURLFilter')); - - const apiURLTitleTooltip = find( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFilter-iconTooltip]' - ); - - await triggerEvent(apiURLTitleTooltip, 'mouseenter'); - - assert - .dom('[data-test-ak-tooltip-content]') - .exists() - .containsText(t('modalCard.dynamicScan.apiScanUrlFilterTooltipText')); - - await triggerEvent(apiURLTitleTooltip, 'mouseleave'); - - if (empty) { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFiltersEmptyContainer]' - ) - .exists() - .containsText(t('modalCard.dynamicScan.emptyAPIListHeaderText')) - .containsText(t('modalCard.dynamicScan.emptyAPIListSubText')); - } else { - apiScanOptions.apiUrlFilterItems.forEach((url) => { - const filterElem = find( - `[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFilter='${url}']` - ); - - assert.dom(filterElem).exists().containsText(url); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFilterIcon]', - filterElem - ) - .exists(); - }); - } - } - ); - - test.each( - 'automated DAST: active scenarios', - [{ emptyActiveList: false }, { emptyActiveList: true }], - async function (assert, { emptyActiveList }) { - assert.expect(); - - 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, - }; - } - ); - - // Scenario Models - const scenarios = this.server.createList('scan-parameter-group', 2, { - project: this.file.project.id, - is_active: emptyActiveList ? false : true, - }); - - await render(hbs` - - - - `); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-modalBodyWrapper]' - ) - .exists(); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-projectScenariosTitle]' - ) - .exists() - .hasText(t('modalCard.dynamicScan.activeScenarios')); - - if (emptyActiveList) { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-scenariosEmptyContainer]' - ) - .exists() - .containsText( - t('modalCard.dynamicScan.emptyActiveScenariosHeaderText') - ) - .containsText( - t('modalCard.dynamicScan.emptyActiveScenariosSubText') - ); - } else { - scenarios.forEach((scenario) => { - const scenarioElem = find( - `[data-test-fileDetails-dynamicScanDrawer-automatedDast-projectScenario='${scenario.id}']` - ); - - assert.dom(scenarioElem).exists().containsText(scenario.name); - - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawer-automatedDast-projectScenarioIcon]', - scenarioElem - ) - .exists(); - }); - } - } - ); - - test.each( - 'test start dynamic scan', - [ - { isAutomated: false, enableApiCapture: false }, - // { isAutomated: true, enableApiCapture: true }, - ], - async function (assert, { isAutomated, enableApiCapture }) { - const file = this.server.create('file', { - project: '1', - profile: '100', - dynamic_status: ENUMS.DYNAMIC_STATUS.NONE, - is_dynamic_done: false, - can_run_automated_dynamicscan: isAutomated, - is_active: true, - }); - - this.set('isAutomated', isAutomated); - - this.set( - 'file', - this.store.push(this.store.normalize('file', file.toJSON())) - ); - - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return { - ...schema.devicePreferences.find(`${req.params.id}`)?.toJSON(), - device_type: ENUMS.DEVICE_TYPE.TABLET_REQUIRED, - }; - }); - - 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( - '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('/profiles/:id/api_scan_options', (_, req) => { - return { - api_url_filters: 'api.example.com,api.example2.com', - id: req.params.id, - }; - }); - - this.server.put( - 'v2/profiles/:id/ds_manual_device_preference', - (_, req) => { - const { ds_manual_device_identifier } = JSON.parse(req.requestBody); - - return { - ds_manual_device_selection: - ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, - ds_manual_device_identifier: ds_manual_device_identifier, - }; - } - ); - - this.server.get('v2/profiles/:id/ds_manual_device_preference', () => { - return { - ds_manual_device_selection: - ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE, - ds_manual_device_identifier: '', - }; - }); - - this.server.post('/v2/files/:id/dynamicscans', (_, req) => { - const reqBody = objectifyEncodedReqBody(req.requestBody); - - assert.strictEqual( - reqBody.mode, - isAutomated ? 'Automated' : 'Manual' - ); - - if (enableApiCapture) { - assert.strictEqual(reqBody.enable_api_capture, 'true'); - } else { - assert.strictEqual(reqBody.enable_api_capture, 'false'); - } - - return new Response(200); - }); - - await render(hbs` - - - - `); - - if (!isAutomated) { - // Since device selection is undefined, start button should be disabled - assert - .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') - .exists() - .isDisabled(); - - // Select "Any Device" - const anyDeviceLabel = t( - dsManualDevicePref([ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE]) - ); - - await selectChoose(`.${classes.trigger}`, anyDeviceLabel); - - if (enableApiCapture) { - // enable api catpure - await click( - '[data-test-fileDetails-dynamicScanDrawer-manualDast-enableAPICaptureCheckbox]' - ); - } - - // Start button should be enabled - assert - .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') - .isNotDisabled(); - - await click('[data-test-fileDetails-dynamicScanDrawer-startBtn]'); - } - } - ); - } -); diff --git a/tests/integration/components/file-details/dynamic-scan/automated-test.js b/tests/integration/components/file-details/dynamic-scan/automated-test.js new file mode 100644 index 000000000..eff0e3f25 --- /dev/null +++ b/tests/integration/components/file-details/dynamic-scan/automated-test.js @@ -0,0 +1,543 @@ +import { render, click, find, 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 { setupBrowserFakes } from 'ember-browser-services/test-support'; +import { module, test } from 'qunit'; +import { faker } from '@faker-js/faker'; + +import ENUMS from 'irene/enums'; +import { dsAutomatedDevicePref } from 'irene/helpers/ds-automated-device-pref'; + +module( + 'Integration | Component | file-details/dynamic-scan/automated', + function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + setupIntl(hooks, 'en'); + setupBrowserFakes(hooks, { window: true, localStorage: true }); + + hooks.beforeEach(async function () { + const store = this.owner.lookup('service:store'); + const dsService = this.owner.lookup('service:dynamic-scan'); + + // Create organization + const organization = this.server.create('organization', { + features: { + dynamicscan_automation: true, + }, + }); + + // Create file + const profile = this.server.create('profile', { + id: '100', + }); + + const file = this.server.create('file', { + id: 1, + project: '1', + profile: profile.id, + is_active: true, + }); + + const project = this.server.create('project', { + file: file.id, + id: '1', + active_profile_id: profile.id, + }); + + const devicePreference = this.server.create( + 'ds-automated-device-preference', + { + id: profile.id, + } + ); + + // 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/profiles/:id/automation_preference', (_, req) => { + return { + id: req.params.id, + dynamic_scan_automation_enabled: true, + }; + }); + + // Set properties + this.setProperties({ + file: store.push(store.normalize('file', file.toJSON())), + project: store.push(store.normalize('project', project.toJSON())), + organization, + devicePreference, + store, + dsService, + }); + + await this.owner.lookup('service:organization').load(); + }); + + test('it renders when dast automation is enabled', async function (assert) { + // Create a dynamic scan + this.server.create('dynamicscan', { + file: this.file.id, + status: ENUMS.DYNAMIC_SCAN_STATUS.NOT_STARTED, + mode: ENUMS.DYNAMIC_MODE.AUTOMATED, + }); + + // In real scenario this will be called in the root component + this.dsService.fetchLatestScans(this.file); + + await render(hbs` + + `); + + assert + .dom('[data-test-fileDetails-dynamicScan-deviceWrapper-headerText]') + .hasText(t('realDevice')); + + // Verify status chip + assert + .dom('[data-test-fileDetails-dynamicScan-statusChip]') + .hasText(t('notStarted')); + + // Verify action buttons + assert + .dom('[data-test-fileDetails-dynamicScanAction]') + .isNotDisabled() + .hasText(t('dastTabs.automatedDAST')); + + // Verfiy vnc + assert + .dom('[data-test-fileDetails-dynamicScan-deviceWrapper-deviceViewer]') + .exists(); + + assert.dom('[data-test-vncViewer-root]').exists(); + }); + + test('it renders upselling when feature is disabled', async function (assert) { + this.organization.update({ + features: { + dynamicscan_automation: false, + }, + }); + + await this.owner.lookup('service:organization').load(); + + // window service + const windowService = this.owner.lookup('service:browser/window'); + + // Stub ajax endpoint + this.server.post('/v2/feature_request/automated_dast', () => { + return {}; + }); + + await render(hbs` + + `); + + assert.dom('[data-test-automated-dast-upselling]').exists(); + + assert + .dom('[data-test-upselling-text]') + .hasText(t('upsellingDastAutomation')); + + assert + .dom('[data-test-upselling-upgrade-now-button]') + .isNotDisabled() + .hasText(t('upgradeNow')); + + assert.dom('[data-test-upselling-upgrade-clicked-text]').doesNotExist(); + + // Click upgrade button + await click('[data-test-upselling-upgrade-now-button]'); + + // Verify post-click state + assert + .dom('[data-test-upselling-upgrade-clicked-text]') + .exists() + .hasText(t('upsellingDastAutomationWhenClicked')); + + assert.dom('[data-test-upselling-upgrade-now-button]').doesNotExist(); + assert.dom('[data-test-upselling-text]').doesNotExist(); + + assert.strictEqual( + windowService.localStorage.getItem('automatedDastRequest'), + 'true' + ); + }); + + test('it renders disabled state when automation preference is disabled', async function (assert) { + // Create automation preference with disabled state + this.server.get('/v2/profiles/:id/automation_preference', (_, req) => { + return { + id: req.params.id, + dynamic_scan_automation_enabled: false, + }; + }); + + await render(hbs` + + `); + + assert + .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledCard]') + .exists(); + + assert + .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledTitle]') + .exists() + .hasText(t('toggleAutomatedDAST')); + + assert + .dom('[data-test-fileDetails-dynamicScan-automatedDast-disabledDesc]') + .exists(); + + assert + .dom( + '[data-test-fileDetails-dynamicScan-automatedDast-disabledActionBtn]' + ) + .exists() + .hasText(t('goToSettings')); + }); + + test.each( + 'it renders action drawer with different states', + [ + { + withApiProxy: true, + hasApiFilters: true, + hasActiveScenarios: true, + assertions: 34, + }, + { + withApiProxy: false, + hasApiFilters: false, + hasActiveScenarios: false, + assertions: 27, + }, + ], + async function ( + assert, + { withApiProxy, hasApiFilters, hasActiveScenarios, assertions } + ) { + assert.expect(assertions); + + this.devicePreference.update({ + ds_automated_device_selection: + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA, + ds_automated_platform_version_min: '10.0', + }); + + const apiUrlFilters = hasApiFilters + ? 'api.example.com,api.example2.com' + : ''; + + // Create a dynamic scan + this.server.create('dynamicscan', { + file: this.file.id, + status: ENUMS.DYNAMIC_SCAN_STATUS.NOT_STARTED, + mode: ENUMS.DYNAMIC_MODE.AUTOMATED, + }); + + const scenarios = this.server.createList('scan-parameter-group', 2, { + project: this.file.project.id, + is_active: hasActiveScenarios, + }); + + this.server.get( + '/v2/profiles/:id/ds_automated_device_preference', + (schema, req) => { + return schema.dsAutomatedDevicePreferences + .find(`${req.params.id}`) + ?.toJSON(); + } + ); + + this.server.get('/profiles/:id', (schema, req) => + schema.profiles.find(`${req.params.id}`)?.toJSON() + ); + + this.server.get('/profiles/:id/api_scan_options', (_, req) => { + return { + api_url_filters: apiUrlFilters, + 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: withApiProxy, + }; + }); + + this.server.get( + '/v2/projects/:projectId/scan_parameter_groups', + function (schema) { + const results = schema.scanParameterGroups.all().models; + return { + count: results.length, + next: null, + previous: null, + results, + }; + } + ); + + // In real scenario this will be called in the root component + this.dsService.fetchLatestScans(this.file); + + await render(hbs` + + `); + + // Click action button to open drawer + await click('[data-test-fileDetails-dynamicScanAction]'); + + // Basic drawer assertions + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-drawerContainer]') + .exists('Drawer container exists'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-drawerContainer-title]' + ) + .hasText(t('dastTabs.automatedDAST'), 'Drawer has correct title'); + + // Device requirements section + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-headerDeviceRequirements]' + ) + .hasText( + t('modalCard.dynamicScan.deviceRequirements'), + 'Device requirements header exists' + ); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-headerOSInfoDesc]' + ) + .hasText( + t('modalCard.dynamicScan.osVersion'), + 'OS version label exists' + ); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-headerOSInfoValue]' + ) + .containsText(this.file.project.get('platformDisplay')) + .containsText(this.file.minOsVersion) + .containsText( + t('modalCard.dynamicScan.orAbove'), + 'OS info shows correct values' + ); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-headerDevicePreference]' + ) + .hasText(t('devicePreferences'), 'Device preferences header exists'); + + // Device Preferences + const devicePrefProps = [ + { + id: 'selectedPref', + title: t('modalCard.dynamicScan.selectedPref'), + value: t( + dsAutomatedDevicePref([ + this.devicePreference.ds_automated_device_selection, + ]) + ), + }, + { + id: 'minOSVersion', + title: t('minOSVersion'), + value: this.devicePreference.ds_automated_platform_version_min, + }, + ].filter(Boolean); + + devicePrefProps.forEach((pref) => { + assert + .dom( + `[data-test-fileDetails-dynamicScanDrawer-automatedDast-devicePreference='${pref.id}']` + ) + .containsText(String(pref.value)) + .containsText(pref.title); + }); + + // API URL Filters section + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiFilterContainer]' + ) + .exists('API filter container exists'); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiFilter-title]' + ) + .exists() + .hasText(t('templates.apiScanURLFilter')); + + const apiURLTitleTooltip = find( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFilter-iconTooltip]' + ); + + await triggerEvent(apiURLTitleTooltip, 'mouseenter'); + + assert + .dom('[data-test-ak-tooltip-content]') + .hasText(t('modalCard.dynamicScan.apiScanUrlFilterTooltipText')); + + await triggerEvent(apiURLTitleTooltip, 'mouseleave'); + + if (hasApiFilters) { + apiUrlFilters.split(',').forEach((url) => { + const filterElem = find( + `[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFilter='${url}']` + ); + + assert.dom(filterElem).hasText(url); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFilterIcon]', + filterElem + ) + .exists(); + }); + } else { + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-apiURLFiltersEmptyContainer]' + ) + .containsText(t('modalCard.dynamicScan.emptyAPIListHeaderText')) + .containsText(t('modalCard.dynamicScan.emptyAPIListSubText')); + } + + // Active scenarios section + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-projectScenariosTitle]' + ) + .hasText(t('modalCard.dynamicScan.activeScenarios')); + + if (hasActiveScenarios) { + scenarios.forEach((scenario) => { + const scenarioElem = find( + `[data-test-fileDetails-dynamicScanDrawer-automatedDast-projectScenario='${scenario.id}']` + ); + + assert.dom(scenarioElem).hasText(scenario.name); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-projectScenarioIcon]', + scenarioElem + ) + .exists(); + }); + } else { + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-scenariosEmptyContainer]' + ) + .containsText( + t('modalCard.dynamicScan.emptyActiveScenariosHeaderText') + ) + .containsText( + t('modalCard.dynamicScan.emptyActiveScenariosSubText') + ); + } + + // Proxy settings section + const proxySetting = this.store.peekRecord( + 'proxy-setting', + this.file.profile.get('id') + ); + + if (withApiProxy) { + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsHeader]' + ) + .containsText(`${t('enable')} ${t('proxySettingsTitle')}`); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsEnabledChip]' + ) + .hasText(t('enabled')); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsRoutingInfo]' + ) + .containsText(t('modalCard.dynamicScan.apiRoutingText')) + .containsText(proxySetting.host); + } else { + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-automatedDast-proxySettingsContainer]' + ) + .doesNotExist(); + } + + // Action buttons + assert + .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') + .isNotDisabled() + .hasText( + t('modalCard.dynamicScan.restartScan'), + 'Start scan button has correct text' + ); + + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-settingsPageRedirectBtn]' + ) + .hasText( + t('modalCard.dynamicScan.goToGeneralSettings'), + 'Settings button has correct text' + ) + .hasAttribute('target', '_blank', 'Settings button opens in new tab') + .hasAttribute( + 'href', + `/dashboard/project/${this.file.project.id}/settings`, + 'Settings button has correct href' + ); + } + ); + } +); 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 a92172a65..3c237700e 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,25 @@ -/* eslint-disable qunit/no-conditional-assertions */ -import { click, fillIn, find, findAll, render } from '@ember/test-helpers'; +import { + click, + find, + findAll, + 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 { selectChoose } from 'ember-power-select/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'; +import dayjs from 'dayjs'; const classes = { dropdown: styles['ak-select-dropdown'], @@ -21,21 +27,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,24 +38,6 @@ 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; - } } module( @@ -72,276 +48,109 @@ 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(); - }); + const store = this.owner.lookup('service:store'); + const dsService = this.owner.lookup('service:dynamic-scan'); - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); - }); + const profile = this.server.create('profile', { id: '100' }); - this.server.get('/profiles/:id/proxy_settings', (_, req) => { - return { - id: req.params.id, - host: faker.internet.ip(), - port: faker.internet.port(), - enabled: false, - }; + const file = this.server.create('file', { + project: '1', + profile: profile.id, + is_active: true, }); - this.server.get('/v2/files/:id', (schema, req) => { - return schema.files.find(`${req.params.id}`)?.toJSON(); + const project = this.server.create('project', { + last_file_id: file.id, + id: '1', }); - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() + const availableDevices = this.server.createList( + 'available-manual-device', + 3 ); - 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 = JSON.parse(req.requestBody); - - this.set('requestBody', data); - - return new Response(200); - }); + 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, + } + ); - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; + // server mocks + this.server.get('/v2/files/:id/dynamicscans', (schema, req) => { + const { limit, mode } = req.queryParams || {}; - return { count: results.length, next: null, previous: null, results }; - }); + const results = schema.dynamicscans + .where({ + file: req.params.id, + ...(mode ? { mode: Number(mode) } : {}), + }) + .models.slice(0, limit ? Number(limit) : results.length); - this.server.get('/profiles/:id/api_scan_options', (_, req) => { return { - api_url_filters: 'api.example.com,api.example2.com', - id: req.params.id, + count: results.length, + next: null, + previous: null, + results, }; }); - 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' }); - - const file = this.server.create('file', { - project: '1', - profile: profile.id, - }); - - const project = this.server.create('project', { - last_file_id: file.id, - id: '1', + this.server.get('/v2/projects/:id', (schema, req) => { + return schema.projects.find(`${req.params.id}`)?.toJSON(); }); - 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, - }), - ]; - - // choose a random device for preference - const randomDevice = faker.helpers.arrayElement( - availableDevices.filter((it) => it.platform === project.platform) - ); + const fileModel = store.push(store.normalize('file', file.toJSON())); - 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, - }); + // In real scenario this will be called in the root component + dsService.fetchLatestScans(fileModel); + // set component properties this.setProperties({ - file: store.push(store.normalize('file', file.toJSON())), + file: fileModel, dynamicScanText: t('modalCard.dynamicScan.title'), profile, project, devicePreference, availableDevices, store, + dsService, }); - 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(); - }); - - await render(hbs` - - - - `); - - if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.ERROR) { - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip]') - .exists() - .hasText(t('errored')); - - 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 - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .doesNotExist(); - } else if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.READY) { - 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 - ) { - 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); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .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 - ) { - assert - .dom('[data-test-fileDetails-dynamicScanAction-restartBtn]') - .isNotDisabled(); - } - } - } - ); - - 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; - - // 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; - } + async function (assert, { withApiProxy, assertions }) { + assert.expect(assertions); - 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 }; }); @@ -349,1366 +158,556 @@ 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')); + .dom('[data-test-fileDetails-proxySettings-enableApiProxyLabel]') + .hasText(`${t('enable')} ${t('proxySettingsTitle')}`); - assert - .dom('[data-test-apiFilter-description]') - .hasText(t('otherTemplates.specifyTheURL')); + const proxySettingsToggle = + '[data-test-fileDetails-proxySettings-enableApiProxyToggle] [data-test-toggle-input]'; - assert - .dom('[data-test-apiFilter-apiEndpointInput]') - .isNotDisabled() - .hasNoValue(); + assert.dom(proxySettingsToggle).isNotDisabled().isNotChecked(); + + const proxySettingsTooltip = find( + '[data-test-fileDetails-proxySettings-helpIcon]' + ); + + await triggerEvent(proxySettingsTooltip, 'mouseenter'); assert - .dom('[data-test-apiFilter-addApiEndpointBtn]') - .isNotDisabled() - .hasText(t('templates.addNewUrlFilter')); + .dom('[data-test-fileDetails-proxySettings-helpTooltipContent]') + .exists() + .containsText(t('proxySettingsRouteVia')) + .containsText(proxySetting.port) + .containsText(proxySetting.host); - assert.dom('[data-test-apiFilter-table]').doesNotExist(); + await triggerEvent(proxySettingsTooltip, 'mouseleave'); } else { assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-apiSettingsContainer]' - ) + .dom('[data-test-fileDetails-proxySettings-container]') .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]') - .hasText(`${t('enable')} ${t('proxySettingsTitle')}`); + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-enableAPICapture]' + ) + .hasText(t('modalCard.dynamicScan.runApiScan')); - assert - .dom('[data-test-proxySettingsView-editSettings]') - .hasTagName('a') - .hasAttribute('href', '/dashboard/project/1/settings') - .hasText(t('edit')); + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-enableAPICaptureCheckbox]' + ) + .isNotChecked(); - assert - .dom('[data-test-proxySettingsView-proxySettingRoute]') - .hasText( - `${t('proxySettingsRouteVia')} ${proxySetting.host}:${proxySetting.port}` - ); - } else { - assert.dom('[data-test-proxySettingsView-container]').doesNotExist(); - } + // Sanity check for API URL filter section (Already tested) + assert + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-apiFilter-title]' + ) + .hasText(t('templates.apiScanURLFilter')); - if (this.file.canRunAutomatedDynamicscan) { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-device-settings-warning]' - ) - .doesNotExist(); + assert.dom('[data-test-apiFilter-description]').doesNotExist(); - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanContainer]' - ) - .exists(); + assert + .dom('[data-test-apiFilter-apiEndpointInput]') + .isNotDisabled() + .hasNoValue(); - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanTitle]' - ) - .hasText(t('dynamicScanAutomation')); + assert + .dom('[data-test-apiFilter-addApiEndpointBtn]') + .isNotDisabled() + .hasText(t('templates.addNewUrlFilter')); - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanChip]' - ) - .hasText(t('experimentalFeature')); + const apiURLTitleTooltip = find( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-apiURLFilter-iconTooltip]' + ); - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanDescription]' - ) - .hasText(t('scheduleDynamicscanDesc')); + await triggerEvent(apiURLTitleTooltip, 'mouseenter'); - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanScheduleBtn]' - ) - .isNotDisabled() - .hasText(t('scheduleDynamicscan')); - } else { - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-device-settings-warning]' - ) - .hasText( - `${t('note')}: ${t('modalCard.dynamicScan.deviceSettingsWarning')}` - ); + assert + .dom('[data-test-ak-tooltip-content]') + .exists() + .containsText(t('modalCard.dynamicScan.apiScanUrlFilterTooltipText')); - assert - .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-automatedDynamicScanContainer]' - ) - .doesNotExist(); - } + await triggerEvent(apiURLTitleTooltip, 'mouseleave'); + // CTA Buttons assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-cancelBtn]') - .isNotDisabled() - .hasText(t('cancel')); + .dom('[data-test-fileDetails-dynamicScanDrawer-startBtn]') + .exists() + .hasText(t('start')); assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]') - .isNotDisabled() - .hasText(t('modalCard.dynamicScan.start')); + .dom('[data-test-fileDetails-dynamicScanDrawer-cancelBtn]') + .exists() + .hasText(t('cancel')); } ); - test('test add & delete of api filter endpoint', async function (assert) { - assert.expect(29); + test('it selects a device preference', async function (assert) { + // start with any device + this.devicePreference.update({ + ds_manual_device_selection: ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE, + ds_manual_device_identifier: '', + }); - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; - this.file.isDynamicDone = false; - this.file.isActive = true; + this.server.get( + '/v2/profiles/:id/ds_manual_device_preference', + (schema, req) => { + return schema.dsManualDevicePreferences + .find(`${req.params.id}`) + ?.toJSON(); + } + ); - this.server.get('/v2/projects/:id', (schema, req) => { - return schema.projects.find(`${req.params.id}`)?.toJSON(); + this.server.put( + '/v2/profiles/:id/ds_manual_device_preference', + (schema, req) => { + return schema.db.dsManualDevicePreferences.update( + req.params.id, + JSON.parse(req.requestBody) + ); + } + ); + + this.server.get('/v2/projects/:id/available_manual_devices', (schema) => { + const results = schema.availableManualDevices.all().models; + + return { count: results.length, next: null, previous: null, results }; }); 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/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, - }; + 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]' - ); + // open the drawer + await click('[data-test-fileDetails-dynamicScanAction="startBtn"]'); assert .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-runApiScanFormControl] [data-test-fileDetails-dynamicScanDrawerOld-runApiScanCheckbox]' + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefHeaderDesc]' ) - .isNotDisabled() - .isChecked(); + .hasText(t('devicePreferences')); assert .dom( - '[data-test-fileDetails-dynamicScanDrawerOld-apiSettingsContainer]' + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefSelect]' ) .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]') - ); - - // confirm box is 2nd modal - assert.dom(findAll('[data-test-ak-appbar]')[1]).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('test enable api proxy toggle', async function (assert) { - assert.expect(10); - - 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(); - }); + .dom( + `[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefSelect] .${classes.trigger}` + ) + .hasText( + t(dsManualDevicePref([ENUMS.DS_MANUAL_DEVICE_SELECTION.ANY_DEVICE])) + ); - this.server.get('/profiles/:id', (schema, req) => - schema.profiles.find(`${req.params.id}`)?.toJSON() + await selectChoose( + `[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefSelect] .${classes.trigger}`, + t( + dsManualDevicePref([ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE]) + ) ); - 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); + .dom( + `[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefSelect] .${classes.trigger}` + ) + .hasText( + t( + dsManualDevicePref([ + ENUMS.DS_MANUAL_DEVICE_SELECTION.SPECIFIC_DEVICE, + ]) + ) + ); 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(); + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-root]' + ) + .exists(); assert .dom( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTableHeaderTitle]' ) - .isNotDisabled() - .isNotChecked(); - - await click( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' - ); + .exists() + .containsText(t('modalCard.dynamicScan.selectSpecificDevice')); assert .dom( - '[data-test-proxySettingsView-enableApiProxyToggle] [data-test-toggle-input]' + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-filterSelect]' ) - .isNotDisabled() - .isChecked(); - - assert.true(proxySetting.enabled); - - const notify = this.owner.lookup('service:notifications'); + .exists(); - assert.strictEqual( - notify.infoMsg, - `${t('proxyTurned')} ${t('on').toUpperCase()}` + // Sanity check for table items + const deviceElemList = findAll( + '[data-test-fileDetails-dynamicScanDrawer-devicePrefTable-row]' ); - }); - - 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, - }); + assert.strictEqual(deviceElemList.length, this.availableDevices.length); - return new Response(200); - }); + const deviceCapabilitiesMap = { + has_sim: 'sim', + has_vpn: 'vpn', + has_pin_lock: 'pinLock', + // has_vnc: 'vnc', + }; - this.server.post( - '/dynamicscan/:id/schedule_automation', - (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.INQUEUE, - }); + const deviceSelectRadioSelector = + '[data-test-fileDetails-dynamicScanDrawer-devicePrefTable-deviceSelectRadioInput]'; - return new Response(201); - } - ); + deviceElemList.forEach((deviceElem, idx) => { + const device = this.availableDevices[idx]; - await render(hbs` - - - - `); - - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .hasText(this.dynamicScanText); + const deviceTypeLabel = deviceType([ + device.is_tablet + ? ENUMS.DEVICE_TYPE.TABLET_REQUIRED + : ENUMS.DEVICE_TYPE.PHONE_REQUIRED, + ]); + // Verify device basic info 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]))); - - 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]') - .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 }); + .dom(deviceElem) + .containsText(device.device_identifier) + .containsText(t(deviceTypeLabel)); - 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, + // Verify device capabilities + Object.entries(deviceCapabilitiesMap).forEach(([key, label]) => { + if (device[key]) { + assert + .dom( + `[data-test-fileDetails-dynamicScanDrawer-devicePrefTable-capabilityId='${label}']`, + deviceElem + ) + .hasText(t(label)); + } }); - return new Response(204); + // Verify radio button state + assert.dom(deviceSelectRadioSelector, deviceElem).isNotChecked(); }); - await render(hbs` - - - - `); + // Select device + await click(deviceElemList[1].querySelector(deviceSelectRadioSelector)); - 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); + // Verify final selection + assert.dom(deviceSelectRadioSelector, deviceElemList[1]).isChecked(); }); test.each( - 'test device unavailability scenarios', + 'it filters devices in device preferences table', [ { - scenario: 'preferred device not available', - removePreferredOnly: true, - expectedError: () => - t('modalCard.dynamicScan.preferredDeviceNotAvailable'), - }, - { - scenario: 'all devices allocated', - removePreferredOnly: false, - expectedError: () => - t('modalCard.dynamicScan.allDevicesAreAllocated'), + label: () => t('modalCard.dynamicScan.devicesWithSim'), + value: 'has_sim', }, { - scenario: 'minimum OS version is unsupported (Android)', - removePreferredOnly: false, - minAndroidOsVersion: 15, // Current min supported android OS is 14 - expectedError: () => - t('modalCard.dynamicScan.minOSVersionUnsupported'), + label: () => t('modalCard.dynamicScan.devicesWithVPN'), + value: 'has_vpn', }, { - scenario: 'minimum OS version is unsupported (iOS)', - removePreferredOnly: false, - minIOSOSVersion: 18, // Current min supported android OS is 17 - expectedError: () => - t('modalCard.dynamicScan.minOSVersionUnsupported'), + label: () => t('modalCard.dynamicScan.devicesWithLock'), + value: 'has_pin_lock', }, ], - 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())) + async function (assert, filter) { + this.server.get( + '/v2/profiles/:id/ds_manual_device_preference', + (schema, req) => { + return schema.dsManualDevicePreferences + .find(`${req.params.id}`) + ?.toJSON(); + } ); - // Server mocks - this.server.get('/v2/projects/:id', (schema, req) => { - const project = schema.projects.find(`${req.params.id}`)?.toJSON(); + this.server.get( + '/v2/projects/:id/available_manual_devices', + (schema, req) => { + const results = schema.availableManualDevices.all().models; - return { - ...project, - platform: minIOSOSVersion - ? ENUMS.PLATFORM.IOS - : minAndroidOsVersion - ? ENUMS.PLATFORM.ANDROID - : project.platform, - }; - }); + this.set('queryParams', req.queryParams); + + return { + count: results.length, + next: null, + previous: null, + results, + }; + } + ); 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, - }; + return { id: req.params.id, host: '', port: '', enabled: false }; }); await render(hbs` - - - - `); + + `); - assert - .dom('[data-test-fileDetails-dynamicScanAction-startBtn]') - .hasText(this.dynamicScanText); + // open the drawer + await click('[data-test-fileDetails-dynamicScanAction="startBtn"]'); assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]') - .doesNotExist(); - - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); + .dom( + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-root]' + ) + .exists(); - // Verify error states assert .dom( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTableHeaderTitle]' ) - .hasClass(classes.triggerError); + .hasText(t('modalCard.dynamicScan.selectSpecificDevice')); assert .dom( - `[data-test-projectPreference-osVersionSelect] .${classes.trigger}` + '[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-filterSelect]' ) - .hasClass(classes.triggerError); + .exists(); + // default all avilable devices selected assert - .dom('[data-test-projectPreference-deviceUnavailableError]') - .hasText(expectedError()); + .dom( + `[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-filterSelect] .${classes.trigger}` + ) + .hasText(t('modalCard.dynamicScan.allAvailableDevices')); + + await selectChoose( + `[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-filterSelect] .${classes.trigger}`, + filter.label() + ); assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]') - .isDisabled(); + .dom( + `[data-test-fileDetails-dynamicScanDrawer-manualDast-devicePrefTable-filterSelect] .${classes.trigger}` + ) + .hasText(filter.label()); + + assert.strictEqual( + this.queryParams[filter.value], + 'true', + `filter ${filter.label()} applied` + ); } ); - 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', + 'dynamic scan extend time', [ - { - 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'], - }, + { canExtend: false, autoShutdownMinutes: 30 }, + { canExtend: true, autoShutdownMinutes: 15 }, ], - 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` - - - - `); + async function (assert, { canExtend, autoShutdownMinutes }) { + this.server.create('dynamicscan', { + file: this.file.id, + mode: ENUMS.DYNAMIC_MODE.MANUAL, + status: ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION, - await click('[data-test-fileDetails-dynamicScanAction-startBtn]'); + auto_shutdown_on: dayjs() + .add(autoShutdownMinutes, 'minutes') + .toISOString(), - // Open device type dropdown - await click( - `[data-test-projectPreference-deviceTypeSelect] .${classes.trigger}` - ); + ended_on: null, + }); - const options = findAll(`.${classes.dropdown} [data-option-index]`); + this.server.get('/v2/dynamicscans/:id', (schema, req) => { + return schema.dynamicscans.find(`${req.params.id}`).toJSON(); + }); - const deviceTypes = Array.from(options).map((el) => - el.textContent.trim() - ); + this.server.put('/v2/dynamicscans/:id/extend', (schema, req) => { + const data = JSON.parse(req.requestBody); - 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'}` - ); - } - ); + assert.strictEqual(data.time, 15); - test('test selects random phone device type and version if any device is selected and resets after scan starts', async function (assert) { - assert.expect(); + return schema.dynamicscans + .find(`${req.params.id}`) + .update({ + auto_shutdown_on: dayjs() + .add(autoShutdownMinutes + data.time, 'minutes') + .toISOString(), + }) + .toJSON(); + }); - const isIOS = ENUMS.PLATFORM.IOS === this.project.platform; + // calling here again since dynamicscan is updated + this.dsService.fetchLatestScans(this.file); - 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 - }); + await render(hbs` + + `); - // update project with latest file - this.project.update({ - last_file_id: file.id, - }); + assert + .dom('[data-test-fileDetails-dynamicScanAction="stopBtn"]') + .isNotDisabled(); - // set the file - this.set( - 'file', - this.store.push(this.store.normalize('file', file.toJSON())) - ); + assert.dom('[data-test-vncviewer-root]').exists(); + assert.dom('[data-test-novncrfb-canvascontainer]').exists(); - // 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, - }; - }); + assert.dom('[data-test-filedetails-dynamicscan-expiry]').exists(); - this.server.put('/dynamicscan/:id', (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.BOOTING, - }); + const expiryInfoTooltip = + '[data-test-filedetails-dynamicscan-expiry-tooltip]'; - return new Response(200); - }); + assert.dom(expiryInfoTooltip).exists(); - this.server.post( - '/dynamicscan/:id/schedule_automation', - (schema, req) => { - schema.db.files.update(`${req.params.id}`, { - dynamic_status: ENUMS.DYNAMIC_STATUS.INQUEUE, - }); + await triggerEvent(expiryInfoTooltip, 'mouseenter'); - return new Response(201); - } - ); - - this.server.put('/profiles/:id/device_preference', (schema, req) => { - const data = JSON.parse(req.requestBody); + assert + .dom('[data-test-ak-tooltip-content]') + .hasText(t('dynamicScanTitleTooltip')); - this.set('requestBody', data); + await triggerEvent(expiryInfoTooltip, 'mouseleave'); - // Preference should be reset after dynamic scan enters an in progress state - if (this.checkPreferenceReset) { - const windowService = this.owner.lookup('service:browser/window'); + assert + .dom('[data-test-filedetails-dynamicscan-expiry-time]') + .containsText(`${autoShutdownMinutes - 1}:`); - const actualDevicePrefData = JSON.parse( - windowService.localStorage.getItem('actualDevicePrefData') - ); + const extendbtn = + '[data-test-filedetails-dynamicscan-expiry-extendbtn]'; - assert.strictEqual( - data.device_type, - actualDevicePrefData.device_type - ); - - assert.strictEqual( - data.platform_version, - String(actualDevicePrefData.platform_version) - ); + if (canExtend) { + assert.dom(extendbtn).isNotDisabled(); + } else { + assert.dom(extendbtn).isDisabled(); } - // 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, - ENUMS.DEVICE_TYPE.PHONE_REQUIRED - ); + await triggerEvent( + '[data-test-fileDetails-dynamicScan-expiry-extendBtn-tooltip]', + 'mouseenter' + ); - this.set('checkPreferenceReset', true); + if (canExtend) { + assert.dom('[data-test-ak-tooltip-content]').doesNotExist(); + } else { + assert + .dom('[data-test-ak-tooltip-content]') + .hasText(t('dynamicScanExtentionLimit')); } - 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); + await triggerEvent( + '[data-test-fileDetails-dynamicScan-expiry-extendBtn-tooltip]', + 'mouseleave' + ); - assert - .dom('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]') - .isNotDisabled() - .hasText(t('modalCard.dynamicScan.start')); + if (canExtend) { + await click(extendbtn); - await click('[data-test-fileDetails-dynamicScanDrawerOld-startBtn]'); + assert + .dom( + '[data-test-filedetails-dynamicscan-expiry-extendtime-menu-item]' + ) + .exists({ count: 3 }); - const notify = this.owner.lookup('service:notifications'); - const poll = this.owner.lookup('service:poll'); + const timeOptions = findAll( + '[data-test-filedetails-dynamicscan-expiry-extendtime-menu-item] button' + ); - assert.strictEqual(notify.successMsg, t('startingScan')); + [5, 15, 30].forEach((time, idx) => { + assert.dom(timeOptions[idx]).hasText(`${time} mins`); + }); - // simulate polling - if (poll.callback) { - await poll.callback(); + await click(timeOptions[1]); + } } - - // 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/dynamic-scan/status-chip-test.js b/tests/integration/components/file-details/dynamic-scan/status-chip-test.js deleted file mode 100644 index d31cc8791..000000000 --- a/tests/integration/components/file-details/dynamic-scan/status-chip-test.js +++ /dev/null @@ -1,107 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { setupIntl, t } from 'ember-intl/test-support'; -import { render } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; - -const dynamicScanStatusText = () => ({ - '-1': t('errored'), - 0: t('notStarted'), - 1: t('deviceInQueue'), - 2: t('deviceBooting'), - 3: t('deviceDownloading'), - 4: t('deviceInstalling'), - 5: t('deviceLaunching'), - 6: t('deviceHooking'), - 7: t('deviceReady'), - 8: t('deviceShuttingDown'), - 9: t('completed'), - 10: t('inProgress'), -}); - -const dynamicScanStatusColor = { - '-1': 'warn', - 0: 'secondary', - 1: 'warn', - 2: 'warn', - 3: 'warn', - 4: 'warn', - 5: 'warn', - 6: 'warn', - 7: 'warn', - 8: 'warn', - 9: 'success', - 10: 'info', -}; - -module( - 'Integration | Component | file-details/dynamic-scan/status-chip', - function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - setupIntl(hooks, 'en'); - - // TODO: Unskip when full DAST feature is ready. - test.skip( - 'it renders status chip for different dynamic scan status', - [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - async function (assert, status) { - const statusText = dynamicScanStatusText(); - - const dynamicscan = this.server.create('dynamicscan', { - id: '1', - mode: 1, - status, - statusText: statusText[status], - ended_on: null, - isDynamicStatusError: status === -1, - isDynamicStatusInProgress: - (status > 0 && status < 9) || status === 10, - isRunning: status === 10, - }); - - const file = this.server.create('file', { - id: '1', - isDynamicDone: status === 9, - }); - - this.setProperties({ - file, - dynamicscan, - }); - - await render( - hbs`` - ); - - assert.dom('[data-test-fileDetails-dynamicScan-statusChip]').exists(); - - const expectedText = statusText[status]; - - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip]') - .hasText(expectedText); - - const expectedColor = dynamicScanStatusColor[status]; - - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip]') - .hasClass(RegExp(`ak-chip-color-${expectedColor}`)); - - if (this.dynamicscan.isDynamicStatusInProgress) { - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip-loader]') - .exists(); - } else { - assert - .dom('[data-test-fileDetails-dynamicScan-statusChip-loader]') - .doesNotExist(); - } - } - ); - } -); 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/integration/components/file-details/scan-actions/dynamic-scan-test.js b/tests/integration/components/file-details/scan-actions/dynamic-scan-test.js index 37bcaf84a..e9e51f319 100644 --- a/tests/integration/components/file-details/scan-actions/dynamic-scan-test.js +++ b/tests/integration/components/file-details/scan-actions/dynamic-scan-test.js @@ -25,11 +25,31 @@ module( this.server.create('project', { file: file.id, id: '1' }); + // set properties this.setProperties({ file: store.push(store.normalize('file', file.toJSON())), }); await this.owner.lookup('service:organization').load(); + + // 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, + }; + }); }); test('it renders dynamic scan title & btn', async function (assert) { @@ -45,10 +65,8 @@ module( }); await render(hbs` - - `); - - assert.dom('[data-test-fileDetailScanActions-scan-type-cards]').exists(); + + `); assert .dom('[data-test-fileDetailScanActions-dynamicScanTitle]') @@ -57,45 +75,176 @@ module( assert .dom('[data-test-fileDetailScanActions-dynamicScanStatus]') .hasText(t('notStarted')); + + assert + .dom('[data-test-fileDetailScanActions-dynamicScanViewDetails]') + .hasText(t('viewDetails')) + .hasAttribute( + 'href', + `/dashboard/file/${this.file.id}/dynamic-scan/manual` + ); }); test.each( 'it renders different states of dynamic scan', [ - { dynamicStatus: ENUMS.DYNAMIC_STATUS.INQUEUE, done: false }, - { dynamicStatus: ENUMS.DYNAMIC_STATUS.DOWNLOADING, done: false }, - { dynamicStatus: ENUMS.DYNAMIC_STATUS.NONE, done: true }, + // No scan scenarios + { + automatedStatus: null, + manualStatus: null, + expectedText: () => t('notStarted'), + }, + + // Running scenarios + { + automatedStatus: null, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION, + expectedText: () => t('inProgress'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.IN_QUEUE, + manualStatus: null, + expectedText: () => t('inProgress'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.AUTO_INTERACTION_COMPLETED, + manualStatus: null, + expectedText: () => t('inProgress'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.IN_QUEUE, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION, + expectedText: () => t('inProgress'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.RUNNING, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION, + expectedText: () => t('inProgress'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.ERROR, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION, + expectedText: () => t('inProgress'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION, + expectedText: () => t('inProgress'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.IN_QUEUE, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + expectedText: () => t('inProgress'), + }, + { + automatedStatus: + ENUMS.DYNAMIC_SCAN_STATUS.INITIATING_AUTO_INTERACTION, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + expectedText: () => t('inProgress'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.IN_QUEUE, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.ERROR, + expectedText: () => t('inProgress'), + }, + { + automatedStatus: + ENUMS.DYNAMIC_SCAN_STATUS.INITIATING_AUTO_INTERACTION, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.ERROR, + expectedText: () => t('inProgress'), + }, + + // Error scenarios + { + automatedStatus: null, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.ERROR, + expectedText: () => t('errored'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.ERROR, + manualStatus: null, + expectedText: () => t('errored'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.ERROR, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.ERROR, + expectedText: () => t('errored'), + }, + + // Completed scenarios + { + automatedStatus: null, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + expectedText: () => t('completed'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + manualStatus: null, + expectedText: () => t('completed'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.ERROR, + expectedText: () => t('completed'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.ERROR, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + expectedText: () => t('completed'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.ANALYSIS_COMPLETED, + expectedText: () => t('completed'), + }, + + // Not started & cancelled scenarios + { + automatedStatus: null, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.CANCELLED, + expectedText: () => t('cancelled'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.CANCELLED, + manualStatus: null, + expectedText: () => t('cancelled'), + }, + { + automatedStatus: ENUMS.DYNAMIC_SCAN_STATUS.CANCELLED, + manualStatus: ENUMS.DYNAMIC_SCAN_STATUS.CANCELLED, + expectedText: () => t('cancelled'), + }, ], - async function (assert, scan) { - this.file.dynamicStatus = scan.dynamicStatus; - this.file.isDynamicDone = scan.done; + async function (assert, { automatedStatus, manualStatus, expectedText }) { + // Create dynamicscan objects in the store + if (automatedStatus) { + this.server.create('dynamicscan', { + id: '1', + file: this.file.id, + mode: ENUMS.DYNAMIC_MODE.AUTOMATED, + status: automatedStatus, + }); + } - this.server.get('/manualscans/:id', (schema, req) => { - return { id: req.params.id }; - }); + if (manualStatus) { + this.server.create('dynamicscan', { + id: '2', + file: this.file.id, + mode: ENUMS.DYNAMIC_MODE.MANUAL, + status: manualStatus, + }); + } await render(hbs` - + `); - if (this.file.isDynamicDone) { - assert - .dom('[data-test-fileDetailScanActions-dynamicScanStatus]') - .exists() - .hasText(t('completed')); - } else if (this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.INQUEUE) { - assert - .dom('[data-test-fileDetailScanActions-dynamicScanStatus]') - .exists() - .hasText(t('deviceInQueue')); - } else if ( - this.file.dynamicStatus === ENUMS.DYNAMIC_STATUS.DOWNLOADING - ) { - assert - .dom('[data-test-fileDetailScanActions-dynamicScanStatus]') - .exists() - .hasText(t('deviceDownloading')); - } + assert + .dom('[data-test-fileDetailScanActions-dynamicScanStatus]') + .exists() + .hasText(expectedText()); } ); } diff --git a/tests/integration/components/project-settings/general-settings/device-preferences-automated-dast-test.js b/tests/integration/components/project-settings/general-settings/device-preferences-automated-dast-test.js index 149843df5..60b769e93 100644 --- a/tests/integration/components/project-settings/general-settings/device-preferences-automated-dast-test.js +++ b/tests/integration/components/project-settings/general-settings/device-preferences-automated-dast-test.js @@ -2,11 +2,13 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupIntl, t } from 'ember-intl/test-support'; -import { render, click, findAll } from '@ember/test-helpers'; +import { render, click } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { selectChoose } from 'ember-power-select/test-support'; +import Service from '@ember/service'; import styles from 'irene/components/ak-select/index.scss'; +import { dsAutomatedDevicePref } from 'irene/helpers/ds-automated-device-pref'; import ENUMS from 'irene/enums'; const classes = { @@ -15,6 +17,19 @@ const classes = { triggerError: styles['ak-select-trigger-error'], }; +class NotificationsStub extends Service { + errorMsg = null; + successMsg = null; + + success(msg) { + this.successMsg = msg; + } + + error(msg) { + this.errorMsg = msg; + } +} + module( 'Integration | Component | project-settings/general-settings/device-preferences-automated-dast', function (hooks) { @@ -23,10 +38,13 @@ module( setupIntl(hooks, 'en'); hooks.beforeEach(async function () { - await this.owner.lookup('service:organization').load(); - this.server.createList('organization', 1); + const availableDevices = this.server.createList( + 'available-automated-device', + 3 + ); + const profile = this.server.create('profile', { id: '1' }); const file = this.server.create('file', { @@ -35,159 +53,304 @@ module( }); const project = this.server.create('project', { - file: file.id, id: '1', - platform: ENUMS.PLATFORM.ANDROID, + file: file.id, + active_profile_id: profile.id, }); + const devicePreference = this.server.create( + 'ds-automated-device-preference', + { id: profile.id } + ); + 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( - 'v2/profiles/:id/ds_manual_device_preference', - () => ({}) + 'v2/profiles/:id/ds_automated_device_preference', + (schema, req) => + schema.dsAutomatedDevicePreferences.find(req.params.id)?.toJSON() ); - this.server.get('v2/profiles/:id/ds_automated_device_preference', () => ({ - ds_automated_device_selection: - ENUMS.DS_AUTOMATED_DEVICE_SELECTION.ANY_DEVICE, - ds_automated_platform_version_min: 13, - })); + this.server.get( + '/v2/projects/:id/available_automated_devices', + (schema) => { + const results = schema.availableAutomatedDevices.all().models; - this.server.get('/projects/:id/available-devices', (schema) => { - const results = schema.projectAvailableDevices.all().models; - return { count: results.length, next: null, previous: null, results }; - }); + return { count: results.length, next: null, previous: null, results }; + } + ); const store = this.owner.lookup('service:store'); - const normalizedProject = store.normalize('project', { - ...project.toJSON(), - }); - this.setProperties({ - project: store.push(normalizedProject), + project: store.push(store.normalize('project', project.toJSON())), + devicePreference, + availableDevices, }); - }); - test('it renders', async function (assert) { - await render(hbs` - - `); + await this.owner.lookup('service:organization').load(); + this.owner.register('service:notifications', NotificationsStub); + }); - assert - .dom( - '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-title]' - ) - .exists() - .containsText(t('devicePreferencesAutomatedDast')); + test.each( + 'it renders', + [ + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.ANY_DEVICE, + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA, + ], + async function (assert, deviceSelection) { + this.devicePreference.update({ + ds_automated_device_selection: deviceSelection, + }); + + await render(hbs` + + + + `); + + assert + .dom( + '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-title]' + ) + .exists() + .containsText(t('devicePreferencesAutomatedDast')); + + assert + .dom( + '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceSelect]' + ) + .exists(); + + assert + .dom( + `[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceSelect] .${classes.trigger}` + ) + .hasText(t(dsAutomatedDevicePref([deviceSelection]))); + + if ( + deviceSelection === + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA + ) { + assert + .dom( + '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteriaContainer]' + ) + .exists(); + + assert + .dom( + '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteria-deviceTypeTitle]' + ) + .exists() + .hasText(t('deviceType')); + + assert + .dom( + `[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteria-deviceTypeRadioGroup] input[value="${this.devicePreference.ds_automated_device_type}"]` + ) + .hasValue(`${this.devicePreference.ds_automated_device_type}`) + .isChecked(); + + assert + .dom( + '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteria-minOSVersionTitle]' + ) + .exists() + .hasText(t('minOSVersion')); + + assert + .dom( + `[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteria-minOSVersionSelect] .${classes.trigger}` + ) + .hasText( + this.devicePreference.ds_automated_platform_version_min || + t('anyVersion') + ); + } else { + assert + .dom( + '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteriaContainer]' + ) + .doesNotExist(); + } + } + ); - assert - .dom( - '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-preference-select]' - ) - .exists(); - }); + test('it selects preference filter criteria', async function (assert) { + this.devicePreference.update({ + ds_automated_device_selection: + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.ANY_DEVICE, + }); - test('it selects criteria', async function (assert) { this.server.put( 'v2/profiles/:id/ds_automated_device_preference', - (_, req) => { + (schema, req) => { const data = JSON.parse(req.requestBody); this.set('requestBody', data); - const { ds_automated_device_selection } = JSON.parse(req.requestBody); - - return { - ds_automated_device_selection: ds_automated_device_selection, - }; + return schema.dsAutomatedDevicePreferences + .find(req.params.id) + .update(data) + .toJSON(); } ); await render(hbs` - + + + `); - let selectListItems = findAll('.ember-power-select-option'); + const notify = this.owner.lookup('service:notifications'); + const devicePreferenceCriteriaSelectTrigger = `[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceSelect] .${classes.trigger}`; - const anyDeviceLabel = t('anyAvailableDeviceWithAnyOS'); + assert + .dom( + '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteriaContainer]' + ) + .doesNotExist(); - // Select "Any Device" - await selectChoose(`.${classes.trigger}`, anyDeviceLabel); + // Select 'Filter Criteria' + const filterCriteriaLabel = t( + dsAutomatedDevicePref([ + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA, + ]) + ); - await click(`.${classes.trigger}`); - selectListItems = findAll('.ember-power-select-option'); + await selectChoose( + devicePreferenceCriteriaSelectTrigger, + filterCriteriaLabel + ); - // "Any Device" is first option - assert.dom(selectListItems[0]).hasAria('selected', 'true'); + // "Filter Criteria" is second option + assert + .dom(devicePreferenceCriteriaSelectTrigger) + .hasText(filterCriteriaLabel); - assert.dom('[data-test-dast-preference-criteria-table]').doesNotExist(); + assert.strictEqual(notify.successMsg, t('savedPreferences')); - // Select 'Specific Device' - const specificDeviceLabel = t('defineDeviceCriteria'); + assert.strictEqual( + this.requestBody.ds_automated_device_selection, + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA + ); - await selectChoose(`.${classes.trigger}`, specificDeviceLabel); + assert + .dom( + '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteriaContainer]' + ) + .exists(); - await click(`.${classes.trigger}`); + const anyDeviceLabel = t( + dsAutomatedDevicePref([ENUMS.DS_AUTOMATED_DEVICE_SELECTION.ANY_DEVICE]) + ); - // "Specific Device" is second option - selectListItems = findAll('.ember-power-select-option'); + // Select "Any Device" + await selectChoose(devicePreferenceCriteriaSelectTrigger, anyDeviceLabel); - assert.dom(selectListItems[1]).hasAria('selected', 'true'); + // "Any Device" is first option + assert.dom(devicePreferenceCriteriaSelectTrigger).hasText(anyDeviceLabel); - await click(`.${classes.trigger}`); + assert + .dom( + '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteriaContainer]' + ) + .doesNotExist(); - assert.strictEqual(this.requestBody.ds_automated_device_selection, 1); + assert.strictEqual(notify.successMsg, t('savedPreferences')); - assert.dom('[data-test-dast-preference-criteria-table]').exists(); + assert.strictEqual( + this.requestBody.ds_automated_device_selection, + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.ANY_DEVICE + ); }); - test('it selects min version', async function (assert) { + test('it selects device type & min version', async function (assert) { + this.devicePreference.update({ + ds_automated_device_type: ENUMS.DS_AUTOMATED_DEVICE_TYPE.NO_PREFERENCE, + ds_automated_platform_version_min: '', + ds_automated_device_selection: + ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA, + }); + + this.availableDevices[1].update({ + platform_version: '12', + }); + this.server.put( 'v2/profiles/:id/ds_automated_device_preference', - (_, req) => { + (schema, req) => { const data = JSON.parse(req.requestBody); this.set('requestBody', data); - const { ds_automated_platform_version_min } = JSON.parse( - req.requestBody - ); - - return { - ds_automated_device_selection: - ENUMS.DS_AUTOMATED_DEVICE_SELECTION.FILTER_CRITERIA, - ds_automated_platform_version_min: - ds_automated_platform_version_min, - }; + return schema.dsAutomatedDevicePreferences + .find(req.params.id) + .update(data) + .toJSON(); } ); await render(hbs` - + + + `); - // Select 'Specific Device' - const specificDeviceLabel = t('defineDeviceCriteria'); + const minOSVersionTrigger = `[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteria-minOSVersionSelect] .${classes.trigger}`; - await selectChoose(`.${classes.trigger}`, specificDeviceLabel); + assert + .dom( + '[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteriaContainer]' + ) + .exists(); - let triggerClass = findAll(`.${classes.trigger}`); + assert + .dom( + `[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteria-deviceTypeRadioGroup] input[value="${ENUMS.DS_AUTOMATED_DEVICE_TYPE.NO_PREFERENCE}"]` + ) + .hasValue(`${ENUMS.DS_AUTOMATED_DEVICE_TYPE.NO_PREFERENCE}`) + .isChecked(); + + assert.dom(minOSVersionTrigger).hasText(t('anyVersion')); + + // select device type + await click( + `[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteria-deviceTypeRadioGroup] input[value="${ENUMS.DS_AUTOMATED_DEVICE_TYPE.PHONE_REQUIRED}"]` + ); - await selectChoose(triggerClass[1], '12'); + assert + .dom( + `[data-test-projectSettings-generalSettings-devicePreferenceAutomatedDast-automatedPreferenceCriteria-deviceTypeRadioGroup] input[value="${ENUMS.DS_AUTOMATED_DEVICE_TYPE.PHONE_REQUIRED}"]` + ) + .hasValue(`${ENUMS.DS_AUTOMATED_DEVICE_TYPE.PHONE_REQUIRED}`) + .isChecked(); - await click(triggerClass[1]); + assert.strictEqual( + this.requestBody.ds_automated_device_type, + ENUMS.DS_AUTOMATED_DEVICE_TYPE.PHONE_REQUIRED + ); - // "12" is third option - let selectListItems = findAll('.ember-power-select-option'); + // select min version + await selectChoose(minOSVersionTrigger, '12'); - assert.dom(selectListItems[2]).hasAria('selected', 'true'); + // "12" is second option + assert.dom(minOSVersionTrigger).hasText('12'); assert.strictEqual( this.requestBody.ds_automated_platform_version_min, diff --git a/tests/integration/components/project-settings/general-settings/dynamicscan-automation-settings/index-test.js b/tests/integration/components/project-settings/general-settings/dynamicscan-automation-settings/index-test.js index 4e6122e66..c9710d237 100644 --- a/tests/integration/components/project-settings/general-settings/dynamicscan-automation-settings/index-test.js +++ b/tests/integration/components/project-settings/general-settings/dynamicscan-automation-settings/index-test.js @@ -23,9 +23,9 @@ const selectors = { dynScanAutoRoot: '[data-test-projectSettings-genSettings-dynScanAutoSettings-root]', dynScanAutoToggle: - '[data-test-genSettings-dynScanAutoSettings-dynamicscanModeToggle] [data-test-toggle-input]', + '[data-test-genSettings-dynScanAutoSettings-dynamicscanAutomationToggle] [data-test-toggle-input]', dynamicscanModeToggleLabel: - '[data-test-genSettings-dynScanAutoSettings-dynamicscanModeToggleLabel]', + '[data-test-genSettings-dynScanAutoSettings-dynamicscanAutomationToggleLabel]', }; module( @@ -45,8 +45,26 @@ module( return schema.files.find(`${req.params.id}`)?.toJSON(); }); - this.server.get('/profiles/:id/dynamicscan_mode', (schema, req) => - schema.dynamicscanModes.find(`${req.queryParams.id}`)?.toJSON() + this.server.create('ds-automation-preference', { + dynamic_scan_automation_enabled: false, + }); + + this.server.get( + '/v2/projects/:projectId/scan_parameter_groups', + function (schema) { + const results = schema.scanParameterGroups.all().models; + + return { + count: results.length, + next: null, + previous: null, + results, + }; + } + ); + + this.server.get('/v2/profiles/:id/automation_preference', (schema, req) => + schema.dsAutomationPreferences.find(`${req.queryParams.id}`)?.toJSON() ); this.owner.register('service:notifications', NotificationsStub); @@ -68,11 +86,6 @@ module( }); test('it renders', async function (assert) { - this.server.create('dynamicscan-mode', { - id: 1, - dynamicscan_mode: 'Manual', - }); - await render(hbs` { + this.server.put('v2/profiles/:id/automation_preference', (_, req) => { const reqBody = JSON.parse(req.requestBody); - this.set('dynamicscan_mode', reqBody.dynamicscan_mode); - return { - dynamicscan_mode: reqBody.dynamicscan_mode, + dynamic_scan_automation_enabled: + reqBody.dynamic_scan_automation_enabled, id: req.params.id, }; }); @@ -124,7 +136,7 @@ module( assert .dom(selectors.dynamicscanModeToggleLabel) .exists() - .containsText(t('appiumScheduledAutomation')); + .containsText(t('enableAutomation')); assert.dom(selectors.dynScanAutoToggle).exists().isNotChecked(); @@ -132,13 +144,15 @@ module( assert.dom(selectors.dynScanAutoToggle).isChecked(); - assert.ok(this.dynamicscan_mode, 'Automated'); + const notify = this.owner.lookup('service:notifications'); + + assert.strictEqual(notify.successMsg, t('scheduledAutomationSuccessOn')); await click(selectors.dynScanAutoToggle); assert.dom(selectors.dynScanAutoToggle).isNotChecked(); - assert.ok(this.dynamicscan_mode, 'Manual'); + assert.strictEqual(notify.successMsg, t('scheduledAutomationSuccessOff')); }); } ); diff --git a/tests/integration/components/vnc-viewer-test.js b/tests/integration/components/vnc-viewer-test.js index a4881df62..f511c5d3e 100644 --- a/tests/integration/components/vnc-viewer-test.js +++ b/tests/integration/components/vnc-viewer-test.js @@ -2,27 +2,14 @@ import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupRenderingTest } from 'ember-qunit'; +import { setupIntl } from 'ember-intl/test-support'; import { module, test } from 'qunit'; -import Service from '@ember/service'; import ENUMS from 'irene/enums'; -class PollServiceStub extends Service { - callback = null; - interval = null; - - startPolling(cb, interval) { - function stop() {} - - this.callback = cb; - this.interval = interval; - - return stop; - } -} - module('Integration | Component | vnc-viewer', function (hooks) { setupRenderingTest(hooks); + setupIntl(hooks, 'en'); setupMirage(hooks); hooks.beforeEach(async function () { @@ -33,6 +20,7 @@ module('Integration | Component | vnc-viewer', function (hooks) { const file = this.server.create('file', { project: '1', profile: profile.id, + is_active: true, }); this.server.create('project', { @@ -41,67 +29,103 @@ module('Integration | Component | vnc-viewer', function (hooks) { active_profile_id: profile.id, }); - const devicePreference = this.server.create('device-preference', { - id: profile.id, - }); - - const dynamicscan = this.server.create('dynamicscan', { - id: profile.id, - status: ENUMS.DYNAMIC_STATUS.NONE, - expires_on: null, - }); - this.setProperties({ file: store.push(store.normalize('file', file.toJSON())), - dynamicScan: dynamicscan, - devicePreference, activeProfileId: profile.id, store, }); - - this.owner.register('service:poll', PollServiceStub); }); test.each( - 'it renders vnc viewer', + 'it renders when not started & device not allocated', [ { platform: ENUMS.PLATFORM.ANDROID, - deviceType: ENUMS.DEVICE_TYPE.TABLET_REQUIRED, - deviceClass: 'tablet', + deviceClass: 'nexus5', }, { platform: ENUMS.PLATFORM.IOS, - deviceType: ENUMS.DEVICE_TYPE.TABLET_REQUIRED, - deviceClass: 'ipad black', - }, - { - platform: ENUMS.PLATFORM.ANDROID, - deviceType: ENUMS.DEVICE_TYPE.PHONE_REQUIRED, - deviceClass: 'nexus5', + deviceClass: 'iphone5s black', }, + ], + async function (assert, { platform, deviceClass }) { + const dynamicscan = this.server.create('dynamicscan', { + file: this.file.id, + status: ENUMS.DYNAMIC_SCAN_STATUS.NOT_STARTED, + ended_on: null, + }); + + this.dynamicscan = this.store.push( + this.store.normalize('dynamicscan', dynamicscan.toJSON()) + ); + + this.server.get('/v2/projects/:id', (schema, req) => { + return { + ...schema.projects.find(`${req.params.id}`)?.toJSON(), + platform, + }; + }); + + await render(hbs` + + `); + + deviceClass.split(' ').forEach((val) => { + assert.dom('[data-test-vncViewer-device]').hasClass(val); + }); + + ['TopBar', 'Sleep', 'Volume'].forEach((it) => { + assert.dom(`[data-test-vncViewer-device${it}]`).doesNotExist(); + }); + + assert.dom('[data-test-vncViewer-deviceCamera]').exists(); + assert.dom('[data-test-vncViewer-deviceScreen]').hasClass('screen'); + + if (platform === ENUMS.PLATFORM.IOS) { + assert.dom('[data-test-vncViewer-deviceHome]').exists(); + + ['Speaker', 'BottomBar'].forEach((it) => { + assert.dom(`[data-test-vncViewer-device${it}]`).doesNotExist(); + }); + } + } + ); + + test.each( + 'it renders when started & device allocated', + [ { platform: ENUMS.PLATFORM.IOS, - deviceType: ENUMS.DEVICE_TYPE.PHONE_REQUIRED, - deviceClass: 'iphone5s black', + isTablet: true, + deviceClass: 'ipad black', }, { platform: ENUMS.PLATFORM.ANDROID, - deviceType: ENUMS.DEVICE_TYPE.NO_PREFERENCE, + isTablet: false, deviceClass: 'nexus5', }, { platform: ENUMS.PLATFORM.IOS, - deviceType: ENUMS.DEVICE_TYPE.NO_PREFERENCE, + isTablet: false, deviceClass: 'iphone5s black', }, ], - async function (assert, { platform, deviceType, deviceClass }) { - this.file.dynamicStatus = ENUMS.DYNAMIC_STATUS.NONE; - this.file.isDynamicDone = true; + async function (assert, { platform, isTablet, deviceClass }) { + const deviceUsed = this.server.create('device', { + is_tablet: isTablet, + platform, + }); - // make sure file is active - this.file.isActive = true; + const dynamicscan = this.server.create('dynamicscan', { + file: this.file.id, + status: ENUMS.DYNAMIC_SCAN_STATUS.READY_FOR_INTERACTION, + ended_on: null, + device_used: deviceUsed.toJSON(), + }); + + this.dynamicscan = this.store.push( + this.store.normalize('dynamicscan', dynamicscan.toJSON()) + ); this.server.get('/v2/projects/:id', (schema, req) => { return { @@ -110,15 +134,8 @@ module('Integration | Component | vnc-viewer', function (hooks) { }; }); - this.server.get('/profiles/:id/device_preference', (schema, req) => { - return { - ...schema.devicePreferences.find(`${req.params.id}`)?.toJSON(), - device_type: deviceType, - }; - }); - await render(hbs` - + `); deviceClass.split(' ').forEach((val) => { @@ -126,7 +143,7 @@ module('Integration | Component | vnc-viewer', function (hooks) { }); ['TopBar', 'Sleep', 'Volume'].forEach((it) => { - if (deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { + if (isTablet) { assert.dom(`[data-test-vncViewer-device${it}]`).exists(); } else { assert.dom(`[data-test-vncViewer-device${it}]`).doesNotExist(); @@ -134,21 +151,13 @@ module('Integration | Component | vnc-viewer', function (hooks) { }); assert.dom('[data-test-vncViewer-deviceCamera]').exists(); - - assert - .dom('[data-test-vncViewer-deviceScreen]') - .hasClass( - platform === ENUMS.PLATFORM.ANDROID && - deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED - ? 'noscreen' - : 'screen' - ); + assert.dom('[data-test-vncViewer-deviceScreen]').hasClass('screen'); if (platform === ENUMS.PLATFORM.IOS) { assert.dom('[data-test-vncViewer-deviceHome]').exists(); ['Speaker', 'BottomBar'].forEach((it) => { - if (deviceType === ENUMS.DEVICE_TYPE.TABLET_REQUIRED) { + if (isTablet) { assert.dom(`[data-test-vncViewer-device${it}]`).exists(); } else { assert.dom(`[data-test-vncViewer-device${it}]`).doesNotExist(); 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/controllers/authenticated/dashboard/file/dynamic-scan/scheduled-automated-test.js b/tests/unit/controllers/authenticated/dashboard/file/dynamic-scan/scheduled-automated-test.js new file mode 100644 index 000000000..b9a83f5d3 --- /dev/null +++ b/tests/unit/controllers/authenticated/dashboard/file/dynamic-scan/scheduled-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/scheduled-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/scheduled-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/tests/unit/services/dynamic-scan-test.js b/tests/unit/services/dynamic-scan-test.js new file mode 100644 index 000000000..63e278d24 --- /dev/null +++ b/tests/unit/services/dynamic-scan-test.js @@ -0,0 +1,250 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import ENUMS from 'irene/enums'; + +module('Unit | Service | dynamic-scan', function (hooks) { + setupTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function () { + this.store = this.owner.lookup('service:store'); + + // Helper function to push a model to store + this.pushToStore = (modelName, serverModel) => { + return this.store.push( + this.store.normalize(modelName, serverModel.toJSON()) + ); + }; + }); + + test('checkScanInProgressAndUpdate does nothing for non-DynamicscanModel', async function (assert) { + const service = this.owner.lookup('service:dynamic-scan'); + + const file = this.pushToStore( + 'file', + this.server.create('file', { id: '100' }) + ); + + // Mock router and current file + service.router = { + currentRouteName: 'authenticated.dashboard.file.dynamic-scan', + }; + + service.currentFile = file; + + // Initial state + service.manualScan = null; + service.automatedScan = null; + + // Perform task with non-model input + await service.checkScanInProgressAndUpdate.perform(file); + + assert.ok(true, 'No error thrown'); + + assert.strictEqual(service.manualScan, null, 'Manual scan remains null'); + + assert.strictEqual( + service.automatedScan, + null, + 'Automated scan remains null' + ); + }); + + test('checkScanInProgressAndUpdate does nothing when not on dynamic scan route', async function (assert) { + const service = this.owner.lookup('service:dynamic-scan'); + + const file = this.pushToStore( + 'file', + this.server.create('file', { id: '100' }) + ); + + // Mock router with wrong route + service.router = { currentRouteName: 'some.other.route' }; + service.currentFile = file; + + const dynamicScan = this.pushToStore( + 'dynamicscan', + this.server.create('dynamicscan', { + file: file.id, + mode: ENUMS.DYNAMIC_MODE.MANUAL, + }) + ); + + // Initial state + service.manualScan = null; + service.automatedScan = null; + + // Perform task + await service.checkScanInProgressAndUpdate.perform(dynamicScan); + + assert.strictEqual(service.manualScan, null, 'Manual scan remains null'); + + assert.strictEqual( + service.automatedScan, + null, + 'Automated scan remains null' + ); + }); + + test('checkScanInProgressAndUpdate does nothing when file does not match', async function (assert) { + const service = this.owner.lookup('service:dynamic-scan'); + + const currentFile = this.pushToStore( + 'file', + this.server.create('file', { id: '200' }) + ); + + // Mock router and current file + service.router = { + currentRouteName: 'authenticated.dashboard.file.dynamic-scan', + }; + + service.currentFile = currentFile; + + const differentFile = this.pushToStore( + 'file', + this.server.create('file', { id: '100' }) + ); + + const dynamicScan = this.pushToStore( + 'dynamicscan', + this.server.create('dynamicscan', { + file: differentFile.id, + mode: ENUMS.DYNAMIC_MODE.MANUAL, + }) + ); + + // Initial state + service.manualScan = null; + service.automatedScan = null; + + // Perform task + await service.checkScanInProgressAndUpdate.perform(dynamicScan); + + assert.strictEqual(service.manualScan, null, 'Manual scan remains null'); + + assert.strictEqual( + service.automatedScan, + null, + 'Automated scan remains null' + ); + }); + + test('checkScanInProgressAndUpdate updates manual scan', async function (assert) { + const service = this.owner.lookup('service:dynamic-scan'); + + service.router = { + currentRouteName: 'authenticated.dashboard.file.dynamic-scan', + }; + + const file = this.pushToStore( + 'file', + this.server.create('file', { id: '100' }) + ); + + service.currentFile = file; + + const dynamicScan = this.pushToStore( + 'dynamicscan', + this.server.create('dynamicscan', { + id: '200', + file: file.id, + mode: ENUMS.DYNAMIC_MODE.MANUAL, + }) + ); + + // Initial state + service.manualScan = null; + + // Perform task + await service.checkScanInProgressAndUpdate.perform(dynamicScan); + + assert.strictEqual( + service.manualScan.id, + dynamicScan.id, + 'Manual scan updated' + ); + }); + + test('checkScanInProgressAndUpdate updates automated scan', async function (assert) { + const service = this.owner.lookup('service:dynamic-scan'); + + service.router = { + currentRouteName: 'authenticated.dashboard.file.dynamic-scan', + }; + + const file = this.pushToStore( + 'file', + this.server.create('file', { id: '100' }) + ); + + service.currentFile = file; + + const dynamicScan = this.pushToStore( + 'dynamicscan', + this.server.create('dynamicscan', { + id: '200', + file: file.id, + mode: ENUMS.DYNAMIC_MODE.AUTOMATED, + }) + ); + + // Initial state + service.automatedScan = null; + + // Perform task + await service.checkScanInProgressAndUpdate.perform(dynamicScan); + + assert.strictEqual( + service.automatedScan.id, + dynamicScan.id, + 'Automated scan updated' + ); + }); + + test('checkScanInProgressAndUpdate does not update scan with same ID', async function (assert) { + const service = this.owner.lookup('service:dynamic-scan'); + + service.router = { + currentRouteName: 'authenticated.dashboard.file.dynamic-scan', + }; + + const file = this.pushToStore( + 'file', + this.server.create('file', { id: '100' }) + ); + + service.currentFile = file; + + const initialDynamicScan = this.pushToStore( + 'dynamicscan', + this.server.create('dynamicscan', { + id: '200', + file: file.id, + mode: ENUMS.DYNAMIC_MODE.MANUAL, + }) + ); + + const duplicateDynamicScan = this.pushToStore( + 'dynamicscan', + this.server.create('dynamicscan', { + id: '200', + file: file.id, + mode: ENUMS.DYNAMIC_MODE.MANUAL, + }) + ); + + // Initial state + service.manualScan = initialDynamicScan; + + // Perform task + await service.checkScanInProgressAndUpdate.perform(duplicateDynamicScan); + + assert.strictEqual( + service.manualScan.id, + initialDynamicScan.id, + 'Scan not updated when ID is the same' + ); + }); +}); diff --git a/translations/en.json b/translations/en.json index 2d965a10f..e6d1f8a34 100644 --- a/translations/en.json +++ b/translations/en.json @@ -82,9 +82,6 @@ "appOrS": "app(s)", "appSwitcher": "App Switcher", "appiumFileUploadedSuccessfully": "File Uploaded Successfully", - "appiumScheduledAutomation": "Schedule Automation", - "appiumScheduledAutomationSuccessOff": "Scheduled dynamic scan automation turned OFF", - "appiumScheduledAutomationSuccessOn": "Scheduled dynamic scan automation turned ON", "appiumScriptInvalid": "Invalid script file. Please upload a valid Appium project", "appiumScripts": "Appium scripts", "appiumScriptsDescription": "As a part of our upcoming automated dynamic scan feature, we are currently supporting scheduled dynamic scans option using Appium scripts. Upload your app's Appium test scripts here as a zip file, dynamic scans would be automatically scheduled for all your future app uploads in this project.", @@ -152,7 +149,8 @@ "authenticatorCodeLabel": "Enter the code from the authenticator app", "authorize": "Authorize", "automated": "Automated", - "automatedScanVncNote": "Automated DAST is running. Preview for the app is not available.", + "automatedScanRunningVncNote": "Automated DAST is running. Preview for the app is not available.", + "automatedScanQueuedVncNote": "Automated DAST request is queued.", "available": "available", "availableCredits": "Available Scan Credits", "azurePipeline": "Azure Pipeline", @@ -167,6 +165,7 @@ "businessImplication": "Business Implication", "by": "by", "cancel": "Cancel", + "cancelled": "Cancelled", "cancelScan": "Cancel Scan", "cancelSubsciption": "Cancel Subscription", "capturedApiEmptyStepsLabel": "Steps to capture API's", @@ -319,6 +318,7 @@ "dastTabs": { "manualDAST": "Manual DAST", "automatedDAST": "Automated DAST", + "scheduledAutomatedDAST": "Scheduled Automated DAST", "dastResults": "DAST Results" }, "dastResultsInfo": "The issues detected for each test cases is cumulative data of all the DAST scan done so far.", @@ -352,7 +352,7 @@ "deviceLaunching": "Launching", "deviceNotFound": "Hard time for us to find the device", "devicePreferences": "Device Preferences", - "devicePreferencesAutomatedDast": "Device Preferences (Automated Dast)", + "devicePreferencesAutomatedDast": "Device Preferences (Automated DAST)", "deviceSelected": "You have successfully selected the device", "deviceShuttingDown": "Stopping", "deviceType": "Device Type", @@ -406,6 +406,7 @@ "emptyURLFilter": "Enter host name", "enHTMLReport": "EN HTML Report", "enable": "Enable", + "enableAutomation": "Enable Automation", "enableMFA": "Enable MFA", "enableMandatoryMFADescription": "By enabling this you are mandating multi factor authentication for all users in this organization. Users who do not have MFA enabled in their user account will receive OTP via the registered email.", "enableMandatoryMFARequirement": "For enabling mandatory MFA for organization, you need to have MFA enabled for your user account. Enable it from here:", @@ -1272,6 +1273,7 @@ "riskOf": "Risk Of", "riskType": "Risk Type", "role": "Role", + "running": "Running", "sama": "SAMA", "samaExpansion": "Saudi Arabian Monetary Authority", "samlAuth": "SAML Authentication", @@ -1375,6 +1377,8 @@ "scheduleDynamicscan": "Schedule Automated Dynamic Scan", "scheduleDynamicscanDesc": "Schedule an automated dynamic scan for this file by clicking on the button below. Once scheduled, the system will use the Active Scenarios on the Project Settings page to execute the Automated DAST. It may take up to 24hrs for this to be completed.", "scheduleDynamicscanSuccess": "Automated dynamic scan scheduled successfully", + "scheduledAutomationSuccessOff": "Dynamic Scan Automation has been turned OFF for this project", + "scheduledAutomationSuccessOn": "Dynamic Scan Automation has been scheduled for this project", "search": "Search", "searchUser": "Search user", "searchProject": "Search project", @@ -1612,7 +1616,8 @@ "threshold": "Threshold", "title": "Title", "toDate": "To Date", - "toggleAutomatedDAST": "Turn on the automated DAST", + "toggleAutomatedDAST": "Turn on Automated DAST for this app", + "toggleAutomatedDASTDesc": "Visit the Settings page to enable Automated DAST", "tokenCopied": "Token Copied!", "toSecureYourMobileApps": "To Secure Your Mobile Applications", "totalProjects": "Total Projects", diff --git a/translations/ja.json b/translations/ja.json index a64974cba..76e4a5827 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -82,9 +82,6 @@ "appOrS": "アプリ", "appSwitcher": "App Switcher", "appiumFileUploadedSuccessfully": "File Uploaded Successfully", - "appiumScheduledAutomation": "Schedule Automation", - "appiumScheduledAutomationSuccessOff": "Scheduled dynamic scan automation turned OFF", - "appiumScheduledAutomationSuccessOn": "Scheduled dynamic scan automation turned ON", "appiumScriptInvalid": "Invalid script file. Please upload a valid Appium project", "appiumScripts": "Appium scripts", "appiumScriptsDescription": "As a part of our upcoming automated dynamic scan feature, we are currently supporting scheduled dynamic scans option using Appium scripts. Upload your app's Appium test scripts here as a zip file, dynamic scans would be automatically scheduled for all your future app uploads in this project.", @@ -152,7 +149,8 @@ "authenticatorCodeLabel": "Enter the code from the authenticator app", "authorize": "Authorize", "automated": "Automated", - "automatedScanVncNote": "Automated DAST is running. Preview for the app is not available.", + "automatedScanRunningVncNote": "Automated DAST is running. Preview for the app is not available.", + "automatedScanQueuedVncNote": "Automated DAST request is queued.", "available": "available", "availableCredits": "Available Scan Credits", "azurePipeline": "Azure Pipeline", @@ -167,6 +165,7 @@ "businessImplication": "ビジネスへの影響", "by": "by", "cancel": "キャンセル", + "cancelled": "Cancelled", "cancelScan": "Cancel Scan", "cancelSubsciption": "サブスクリプションのキャンセル", "capturedApiEmptyStepsLabel": "Steps to capture API's", @@ -319,6 +318,7 @@ "dastTabs": { "manualDAST": "Manual DAST", "automatedDAST": "Automated DAST", + "scheduledAutomatedDAST": "Scheduled Automated DAST", "dastResults": "DAST Results" }, "dastResultsInfo": "The issues detected for each test cases is cumulative data of all the DAST scan done so far.", @@ -352,7 +352,7 @@ "deviceLaunching": "開始中", "deviceNotFound": "デバイスが見つかりませんでした", "devicePreferences": "デバイスの設定", - "devicePreferencesAutomatedDast": "Device Preferences (Automated Dast)", + "devicePreferencesAutomatedDast": "Device Preferences (Automated DAST)", "deviceSelected": "デバイスを選択しました", "deviceShuttingDown": "停止中", "deviceType": "デバイスタイプ", @@ -406,6 +406,7 @@ "emptyURLFilter": "Enter host name", "enHTMLReport": "EN HTML Report", "enable": "Enable", + "enableAutomation": "Enable Automation", "enableMFA": "MFAの有効化", "enableMandatoryMFADescription": "By enabling this you are mandating multi factor authentication for all users in this organization. Users who do not have MFA enabled in their user account will receive OTP via the registered email.", "enableMandatoryMFARequirement": "For enabling mandatory MFA for organization, you need to have MFA enabled for your user account. Enable it from here:", @@ -1272,6 +1273,7 @@ "riskOf": "リスク", "riskType": "リスクタイプ", "role": "役割", + "running": "Running", "sama": "SAMA", "samaExpansion": "Saudi Arabian Monetary Authority", "samlAuth": "SAML Authentication", @@ -1375,6 +1377,8 @@ "scheduleDynamicscan": "Schedule Automated Dynamic Scan", "scheduleDynamicscanDesc": "Schedule an automated dynamic scan for this file by clicking on the button below. Once scheduled, the system will use the Active Scenarios on the Project Settings page to execute the Automated DAST. It may take up to 24hrs for this to be completed.", "scheduleDynamicscanSuccess": "Automated dynamic scan scheduled successfully", + "scheduledAutomationSuccessOff": "Dynamic Scan Automation has been turned OFF for this project", + "scheduledAutomationSuccessOn": "Dynamic Scan Automation has been scheduled for this project", "search": "検索", "searchUser": "Search user", "searchProject": "Search project", @@ -1612,7 +1616,8 @@ "threshold": "Threshold", "title": "Title", "toDate": "To Date", - "toggleAutomatedDAST": "Turn on the automated DAST", + "toggleAutomatedDAST": "Turn on Automated DAST for this app", + "toggleAutomatedDASTDesc": "Visit the Settings page to enable Automated DAST", "tokenCopied": "トークンがコピーされました", "toSecureYourMobileApps": "To Secure Your Mobile Applications", "totalProjects": "Total Projects",