Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
f48d1a32
Commit
f48d1a32
authored
Dec 29, 2021
by
Paul Gascou-Vaillancourt
Committed by
Natalia Tepluhina
Dec 29, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add saved scans actions
parent
3d2e8da5
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
541 additions
and
43 deletions
+541
-43
ee/app/assets/javascripts/on_demand_scans/components/on_demand_scans.vue
...avascripts/on_demand_scans/components/on_demand_scans.vue
+4
-4
ee/app/assets/javascripts/on_demand_scans/components/tabs/base_tab.vue
.../javascripts/on_demand_scans/components/tabs/base_tab.vue
+10
-20
ee/app/assets/javascripts/on_demand_scans/components/tabs/saved.vue
...ets/javascripts/on_demand_scans/components/tabs/saved.vue
+208
-2
ee/app/assets/javascripts/on_demand_scans/graphql/cache_utils.js
...assets/javascripts/on_demand_scans/graphql/cache_utils.js
+13
-0
ee/app/assets/javascripts/on_demand_scans/mixins/handles_errors.js
...sets/javascripts/on_demand_scans/mixins/handles_errors.js
+30
-0
ee/spec/frontend/on_demand_scans/components/on_demand_scans_spec.js
...ontend/on_demand_scans/components/on_demand_scans_spec.js
+2
-2
ee/spec/frontend/on_demand_scans/components/tabs/base_tab_spec.js
...frontend/on_demand_scans/components/tabs/base_tab_spec.js
+3
-6
ee/spec/frontend/on_demand_scans/components/tabs/saved_spec.js
...ec/frontend/on_demand_scans/components/tabs/saved_spec.js
+221
-9
ee/spec/frontend/on_demand_scans/graphql/cache_utils_spec.js
ee/spec/frontend/on_demand_scans/graphql/cache_utils_spec.js
+35
-0
locale/gitlab.pot
locale/gitlab.pot
+15
-0
No files found.
ee/app/assets/javascripts/on_demand_scans/components/on_demand_scans.vue
View file @
f48d1a32
<
script
>
import
{
GlButton
,
GlLink
,
GlSprintf
,
GlTabs
}
from
'
@gitlab/ui
'
;
import
{
GlButton
,
GlLink
,
GlSprintf
,
Gl
Scrollable
Tabs
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
ConfigurationPageLayout
from
'
ee/security_configuration/components/configuration_page_layout.vue
'
;
import
{
...
...
@@ -27,7 +27,7 @@ export default {
GlButton
,
GlLink
,
GlSprintf
,
GlTabs
,
Gl
Scrollable
Tabs
,
ConfigurationPageLayout
,
AllTab
,
RunningTab
,
...
...
@@ -159,7 +159,7 @@ export default {
</
template
>
</gl-sprintf>
</template>
<gl-
tabs
v-model=
"activeTab
"
>
<gl-
scrollable-tabs
v-model=
"activeTab"
data-testid=
"on-demand-scans-tabs
"
>
<component
:is=
"tab.component"
v-for=
"(tab, key, index) in tabs"
...
...
@@ -167,7 +167,7 @@ export default {
:items-count=
"tab.itemsCount"
:is-active=
"activeTab === index"
/>
</gl-tabs>
</gl-
scrollable-
tabs>
</configuration-page-layout>
<empty-state
v-else
/>
</template>
ee/app/assets/javascripts/on_demand_scans/components/tabs/base_tab.vue
View file @
f48d1a32
<
script
>
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
{
GlTab
,
GlBadge
,
...
...
@@ -15,7 +14,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import
{
DAST_SHORT_NAME
}
from
'
~/security_configuration/components/constants
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
scrollToElement
}
from
'
~/lib/utils/common_util
s
'
;
import
handlesErrors
from
'
../../mixins/handles_error
s
'
;
import
Actions
from
'
../actions.vue
'
;
import
EmptyState
from
'
../empty_state.vue
'
;
import
{
PIPELINES_PER_PAGE
,
PIPELINES_POLL_INTERVAL
,
ACTION_COLUMN
}
from
'
../../constants
'
;
...
...
@@ -45,6 +44,7 @@ export default {
Actions
,
EmptyState
,
},
mixins
:
[
handlesErrors
],
inject
:
[
'
projectPath
'
],
props
:
{
isActive
:
{
...
...
@@ -127,7 +127,6 @@ export default {
return
{
cursor
,
hasError
:
false
,
actionErrorMessage
:
''
,
};
},
computed
:
{
...
...
@@ -189,19 +188,6 @@ export default {
});
this
.
resetActionError
();
},
handleActionError
(
message
,
exception
=
null
)
{
this
.
actionErrorMessage
=
message
;
this
.
scrollToTop
();
if
(
exception
!==
null
)
{
Sentry
.
captureException
(
exception
);
}
},
resetActionError
()
{
this
.
actionErrorMessage
=
''
;
},
scrollToTop
()
{
scrollToElement
(
this
.
$el
);
},
},
i18n
:
{
previousPage
:
__
(
'
Prev
'
),
...
...
@@ -216,8 +202,10 @@ export default {
<
template
>
<gl-tab
v-bind=
"$attrs"
>
<template
#title
>
{{
title
}}
<gl-badge
size=
"sm"
class=
"gl-tab-counter-badge"
>
{{
itemsCount
}}
</gl-badge>
<span
class=
"gl-white-space-nowrap"
>
{{
title
}}
<gl-badge
size=
"sm"
class=
"gl-tab-counter-badge"
>
{{
itemsCount
}}
</gl-badge>
</span>
</
template
>
<
template
v-if=
"$apollo.queries.pipelines.loading || hasPipelines"
>
<gl-table
...
...
@@ -243,10 +231,10 @@ export default {
</gl-skeleton-loader>
</
template
>
<
template
v-if=
"
actionErrorMessage
"
#top-row
>
<
template
v-if=
"
hasActionError || $scopedSlots.error
"
#top-row
>
<td
:colspan=
"tableFields.length"
>
<gl-alert
class=
"gl-my-4"
variant=
"danger"
:dismissible=
"false"
>
{{
actionErrorMessage
}}
<slot
name=
"error"
>
{{
actionErrorMessage
}}
</slot>
</gl-alert>
</td>
</
template
>
...
...
@@ -308,6 +296,8 @@ export default {
@
next=
"nextPage"
/>
</div>
<slot></slot>
</template>
<gl-alert
v-else-if=
"hasError"
...
...
ee/app/assets/javascripts/on_demand_scans/components/tabs/saved.vue
View file @
f48d1a32
<
script
>
import
{
GlIcon
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
GlButton
,
GlDropdown
,
GlDropdownItem
,
GlIcon
,
GlModal
,
GlTooltipDirective
,
}
from
'
@gitlab/ui
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
{
redirectTo
}
from
'
~/lib/utils/url_utility
'
;
import
ScanTypeBadge
from
'
ee/security_configuration/dast_profiles/components/dast_scan_type_badge.vue
'
;
import
dastProfileRunMutation
from
'
ee/security_configuration/dast_profiles/graphql/dast_profile_run.mutation.graphql
'
;
import
dastProfileDelete
from
'
ee/security_configuration/dast_profiles/graphql/dast_profile_delete.mutation.graphql
'
;
import
handlesErrors
from
'
../../mixins/handles_errors
'
;
import
{
removeProfile
}
from
'
../../graphql/cache_utils
'
;
import
dastProfilesQuery
from
'
../../graphql/dast_profiles.query.graphql
'
;
import
{
SAVED_TAB_TABLE_FIELDS
,
LEARN_MORE_TEXT
}
from
'
../../constants
'
;
import
BaseTab
from
'
./base_tab.vue
'
;
...
...
@@ -9,15 +21,124 @@ import BaseTab from './base_tab.vue';
export
default
{
query
:
dastProfilesQuery
,
components
:
{
GlButton
,
GlDropdown
,
GlDropdownItem
,
GlIcon
,
GlModal
,
BaseTab
,
ScanTypeBadge
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
mixins
:
[
handlesErrors
],
inject
:
[
'
projectPath
'
],
tableFields
:
SAVED_TAB_TABLE_FIELDS
,
deleteScanModalId
:
`delete-scan-modal`
,
i18n
:
{
title
:
s__
(
'
OnDemandScans|Scan library
'
),
emptyStateTitle
:
s__
(
'
OnDemandScans|There are no saved scans.
'
),
emptyStateText
:
LEARN_MORE_TEXT
,
actions
:
__
(
'
Actions
'
),
moreActions
:
__
(
'
More actions
'
),
runScan
:
s__
(
'
OnDemandScans|Run scan
'
),
runScanError
:
s__
(
'
OnDemandScans|Could not run the scan. Please try again.
'
),
editProfile
:
s__
(
'
OnDemandScans|Edit profile
'
),
editButtonLabel
:
__
(
'
Edit
'
),
deleteModalTitle
:
s__
(
'
OnDemandScans|Are you sure you want to delete this scan?
'
),
deleteButtonLabel
:
__
(
'
Delete
'
),
deleteProfile
:
s__
(
'
OnDemandScans|Delete profile
'
),
deletionError
:
s__
(
'
OnDemandScans|Could not delete saved scan. Please refresh the page, or try again later.
'
,
),
},
data
()
{
return
{
runningScanId
:
null
,
deletingScanId
:
null
,
};
},
methods
:
{
async
runScan
({
id
})
{
this
.
resetActionError
();
this
.
runningScanId
=
id
;
try
{
const
{
data
:
{
dastProfileRun
:
{
pipelineUrl
,
errors
},
},
}
=
await
this
.
$apollo
.
mutate
({
mutation
:
dastProfileRunMutation
,
variables
:
{
input
:
{
id
,
},
},
});
if
(
errors
.
length
)
{
this
.
handleActionError
(
errors
[
0
]);
this
.
runningScanId
=
null
;
}
else
{
redirectTo
(
pipelineUrl
);
}
}
catch
(
exception
)
{
this
.
handleActionError
(
this
.
$options
.
i18n
.
runScanError
,
exception
);
this
.
runningScanId
=
null
;
}
},
prepareProfileDeletion
(
profileId
)
{
this
.
deletingScanId
=
profileId
;
this
.
$refs
[
this
.
$options
.
deleteScanModalId
].
show
();
},
async
deleteProfile
()
{
this
.
resetActionError
();
try
{
await
this
.
$apollo
.
mutate
({
mutation
:
dastProfileDelete
,
variables
:
{
input
:
{
id
:
this
.
deletingScanId
,
},
},
update
:
(
store
,
{
data
=
{}
})
=>
{
const
errors
=
data
.
dastProfileDelete
?.
errors
??
[];
if
(
errors
.
length
)
{
this
.
handleActionError
(
errors
[
0
]);
}
else
{
removeProfile
({
profileId
:
this
.
deletingScanId
,
store
,
queryBody
:
{
query
:
dastProfilesQuery
,
variables
:
{
fullPath
:
this
.
projectPath
,
},
},
});
}
},
optimisticResponse
:
{
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename
:
'
Mutation
'
,
dastProfileDelete
:
{
__typename
:
'
DastProfileDeletePayload
'
,
errors
:
[],
},
},
});
}
catch
(
exception
)
{
this
.
handleActionError
(
this
.
$options
.
i18n
.
deletionError
,
exception
);
}
},
cancelDeletion
()
{
this
.
deletingScanId
=
null
;
},
},
};
</
script
>
...
...
@@ -32,10 +153,95 @@ export default {
:empty-state-text=
"$options.i18n.emptyStateText"
v-bind=
"$attrs"
>
<template
v-if=
"hasActionError"
#error
>
{{
actionErrorMessage
}}
</
template
>
<
template
#after-name=
"item"
><gl-icon
name=
"branch"
/>
{{
item
.
branch
.
name
}}
</
template
>
<
template
#cell(scanType)=
"{ value }"
>
<scan-type-badge
:scan-type=
"value"
/>
</
template
>
<
template
#cell(actions)=
"{ item }"
>
<div
class=
"gl-text-right"
>
<gl-button
size=
"small"
data-testid=
"dast-scan-run-button"
:loading=
"runningScanId === item.id"
:disabled=
"Boolean(runningScanId)"
@
click=
"runScan(item)"
>
{{
$options
.
i18n
.
runScan
}}
</gl-button>
<!-- More actions for desktop -->
<gl-dropdown
v-gl-tooltip
:text=
"$options.i18n.moreActions"
:title=
"$options.i18n.moreActions"
category=
"tertiary"
size=
"small"
icon=
"ellipsis_v"
toggle-class=
"gl-border-0! gl-shadow-none! gl-pl-2! gl-pr-2!"
class=
"gl-display-none gl-md-display-inline-flex!"
no-caret
right
text-sr-only
>
<gl-dropdown-item
:href=
"item.editPath"
:aria-label=
"$options.i18n.editProfile"
data-testid=
"edit-scan-button-desktop"
>
{{
$options
.
i18n
.
editButtonLabel
}}
</gl-dropdown-item>
<gl-dropdown-item
:aria-label=
"$options.i18n.deleteProfile"
boundary=
"viewport"
variant=
"danger"
data-testid=
"delete-scan-button-desktop"
@
click=
"prepareProfileDeletion(item.id)"
>
{{
$options
.
i18n
.
deleteButtonLabel
}}
</gl-dropdown-item>
</gl-dropdown>
<!-- More actions for mobile -->
<gl-button
:href=
"item.editPath"
:aria-label=
"$options.i18n.editProfile"
category=
"tertiary"
class=
"gl-md-display-none"
size=
"small"
data-testid=
"edit-scan-button-mobile"
>
{{
$options
.
i18n
.
editButtonLabel
}}
</gl-button>
<gl-button
category=
"tertiary"
icon=
"remove"
variant=
"danger"
size=
"small"
class=
"gl-md-display-none"
data-testid=
"delete-scan-button-mobile"
:aria-label=
"$options.i18n.deleteProfile"
@
click=
"prepareProfileDeletion(item.id)"
/>
</div>
</
template
>
<gl-modal
:ref=
"$options.deleteScanModalId"
:modal-id=
"$options.deleteScanModalId"
:title=
"$options.i18n.deleteModalTitle"
:ok-title=
"$options.i18n.deleteButtonLabel"
ok-variant=
"danger"
body-class=
"gl-display-none"
lazy
@
ok=
"deleteProfile"
@
cancel=
"cancelDeletion"
/>
</base-tab>
</template>
ee/app/assets/javascripts/on_demand_scans/graphql/cache_utils.js
0 → 100644
View file @
f48d1a32
import
{
produce
}
from
'
immer
'
;
export
const
removeProfile
=
({
profileId
,
store
,
queryBody
})
=>
{
const
sourceData
=
store
.
readQuery
(
queryBody
);
const
data
=
produce
(
sourceData
,
(
draftState
)
=>
{
draftState
.
project
.
pipelines
.
nodes
=
draftState
.
project
.
pipelines
.
nodes
.
filter
(({
id
})
=>
{
return
id
!==
profileId
;
});
});
store
.
writeQuery
({
...
queryBody
,
data
});
};
ee/app/assets/javascripts/on_demand_scans/mixins/handles_errors.js
0 → 100644
View file @
f48d1a32
import
*
as
Sentry
from
'
@sentry/browser
'
;
import
{
scrollToElement
}
from
'
~/lib/utils/common_utils
'
;
export
default
{
data
()
{
return
{
actionErrorMessage
:
''
,
};
},
computed
:
{
hasActionError
()
{
return
Boolean
(
this
.
actionErrorMessage
.
length
);
},
},
methods
:
{
handleActionError
(
message
,
exception
=
null
)
{
this
.
actionErrorMessage
=
message
;
this
.
scrollToTop
();
if
(
exception
!==
null
)
{
Sentry
.
captureException
(
exception
);
}
},
resetActionError
()
{
this
.
actionErrorMessage
=
''
;
},
scrollToTop
()
{
scrollToElement
(
this
.
$el
);
},
},
};
ee/spec/frontend/on_demand_scans/components/on_demand_scans_spec.js
View file @
f48d1a32
...
...
@@ -40,7 +40,7 @@ describe('OnDemandScans', () => {
// Finders
const
findNewScanLink
=
()
=>
wrapper
.
findByTestId
(
'
new-scan-link
'
);
const
findHelpPageLink
=
()
=>
wrapper
.
findByTestId
(
'
help-page-link
'
);
const
findTabs
=
()
=>
wrapper
.
find
Component
(
GlTabs
);
const
findTabs
=
()
=>
wrapper
.
find
ByTestId
(
'
on-demand-scans-tabs
'
);
const
findAllTab
=
()
=>
wrapper
.
findComponent
(
AllTab
);
const
findRunningTab
=
()
=>
wrapper
.
findComponent
(
RunningTab
);
const
findFinishedTab
=
()
=>
wrapper
.
findComponent
(
FinishedTab
);
...
...
@@ -68,7 +68,7 @@ describe('OnDemandScans', () => {
stubs
:
{
ConfigurationPageLayout
,
GlSprintf
,
GlTabs
,
Gl
ScrollableTabs
:
Gl
Tabs
,
},
},
{
...
...
ee/spec/frontend/on_demand_scans/components/tabs/base_tab_spec.js
View file @
f48d1a32
...
...
@@ -305,21 +305,18 @@ describe('BaseTab', () => {
});
});
it
(
'
renders the after-name slot
'
,
async
(
)
=>
{
it
.
each
([
'
default
'
,
'
after-name
'
,
'
error
'
])(
'
renders the %s slot
'
,
async
(
slot
)
=>
{
createFullComponent
({
propsData
:
{
itemsCount
:
30
,
},
stubs
:
{
GlTable
:
false
,
},
scopedSlots
:
{
'
after-name
'
:
'
<div data-testid="after-name-content" />
'
,
[
slot
]:
`<div data-testid="
${
slot
}
-slot-content" />`
,
},
});
await
waitForPromises
();
expect
(
wrapper
.
findByTestId
(
'
after-name-content
'
).
exists
()).
toBe
(
true
);
expect
(
wrapper
.
findByTestId
(
`
${
slot
}
-slot-content`
).
exists
()).
toBe
(
true
);
});
describe
(
"
when a scan's DAST profile got deleted
"
,
()
=>
{
...
...
ee/spec/frontend/on_demand_scans/components/tabs/saved_spec.js
View file @
f48d1a32
...
...
@@ -7,19 +7,35 @@ import SavedTab from 'ee/on_demand_scans/components/tabs/saved.vue';
import
BaseTab
from
'
ee/on_demand_scans/components/tabs/base_tab.vue
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
dastProfilesQuery
from
'
ee/on_demand_scans/graphql/dast_profiles.query.graphql
'
;
import
dastProfileRunMutation
from
'
ee/security_configuration/dast_profiles/graphql/dast_profile_run.mutation.graphql
'
;
import
dastProfileDeleteMutation
from
'
ee/security_configuration/dast_profiles/graphql/dast_profile_delete.mutation.graphql
'
;
import
{
createRouter
}
from
'
ee/on_demand_scans/router
'
;
import
{
SAVED_TAB_TABLE_FIELDS
,
LEARN_MORE_TEXT
}
from
'
ee/on_demand_scans/constants
'
;
import
{
s__
}
from
'
~/locale
'
;
import
ScanTypeBadge
from
'
ee/security_configuration/dast_profiles/components/dast_scan_type_badge.vue
'
;
jest
.
mock
(
'
~/lib/utils/common_utils
'
)
;
import
flushPromises
from
'
helpers/flush_promises
'
;
import
{
redirectTo
}
from
'
~/lib/utils/url_utility
'
;
Vue
.
use
(
VueApollo
);
// Mocks
jest
.
mock
(
'
~/lib/utils/common_utils
'
);
jest
.
mock
(
'
~/lib/utils/url_utility
'
);
const
[
firstProfile
]
=
dastProfilesMock
.
data
.
project
.
pipelines
.
nodes
;
const
GlTableMock
=
{
firstProfile
,
template
:
`
<div>
<slot name="cell(actions)" :item="$options.firstProfile" />
<slot name="error" />
</div>`
,
};
const
errorAsDataMessage
=
'
Error-as-data message
'
;
describe
(
'
Saved tab
'
,
()
=>
{
let
wrapper
;
let
router
;
let
requestHandler
;
let
requestHandler
s
;
// Props
const
projectPath
=
'
/namespace/project
'
;
...
...
@@ -29,11 +45,32 @@ describe('Saved tab', () => {
const
findBaseTab
=
()
=>
wrapper
.
findComponent
(
BaseTab
);
const
findFirstRow
=
()
=>
wrapper
.
find
(
'
tbody > tr
'
);
const
findCellAt
=
(
index
)
=>
findFirstRow
().
findAll
(
'
td
'
).
at
(
index
);
const
findRunScanButton
=
()
=>
wrapper
.
findByTestId
(
'
dast-scan-run-button
'
);
const
findDeleteModal
=
()
=>
wrapper
.
findComponent
({
ref
:
'
delete-scan-modal
'
});
// Helpers
const
createMockApolloProvider
=
()
=>
{
return
createMockApollo
([[
dastProfilesQuery
,
requestHandler
]]);
return
createMockApollo
([
[
dastProfilesQuery
,
requestHandlers
.
dastProfilesQuery
],
[
dastProfileRunMutation
,
requestHandlers
.
dastProfileRunMutation
],
[
dastProfileDeleteMutation
,
requestHandlers
.
dastProfileDeleteMutation
],
]);
};
const
makeDastProfileRunResponse
=
(
errors
=
[])
=>
({
data
:
{
dastProfileRun
:
{
pipelineUrl
:
'
/pipelines/1
'
,
errors
,
},
},
});
const
makeDastProfileDeleteResponse
=
(
errors
=
[])
=>
({
data
:
{
dastProfileDelete
:
{
errors
,
},
},
});
const
createComponentFactory
=
(
mountFn
=
shallowMountExtended
)
=>
(
options
=
{})
=>
{
router
=
createRouter
();
...
...
@@ -63,13 +100,17 @@ describe('Saved tab', () => {
const
createFullComponent
=
createComponentFactory
(
mountExtended
);
beforeEach
(()
=>
{
requestHandler
=
jest
.
fn
().
mockResolvedValue
(
dastProfilesMock
);
requestHandlers
=
{
dastProfilesQuery
:
jest
.
fn
().
mockResolvedValue
(
dastProfilesMock
),
dastProfileRunMutation
:
jest
.
fn
().
mockResolvedValue
(
makeDastProfileRunResponse
()),
dastProfileDeleteMutation
:
jest
.
fn
().
mockResolvedValue
(
makeDastProfileDeleteResponse
()),
};
});
afterEach
(()
=>
{
wrapper
.
destroy
();
router
=
null
;
requestHandler
=
null
;
requestHandler
s
=
null
;
});
it
(
'
renders the base tab with the correct props
'
,
()
=>
{
...
...
@@ -88,7 +129,7 @@ describe('Saved tab', () => {
it
(
'
fetches the profiles
'
,
()
=>
{
createComponent
();
expect
(
requestHandler
).
toHaveBeenCalledWith
({
expect
(
requestHandler
s
.
dastProfilesQuery
).
toHaveBeenCalledWith
({
after
:
null
,
before
:
null
,
first
:
20
,
...
...
@@ -98,8 +139,6 @@ describe('Saved tab', () => {
});
describe
(
'
custom table cells
'
,
()
=>
{
const
[
firstProfile
]
=
dastProfilesMock
.
data
.
project
.
pipelines
.
nodes
;
beforeEach
(()
=>
{
createFullComponent
();
});
...
...
@@ -117,4 +156,177 @@ describe('Saved tab', () => {
expect
(
firstScanTypeBadge
.
props
(
'
scanType
'
)).
toBe
(
firstProfile
.
dastScannerProfile
.
scanType
);
});
});
describe
(
'
edit button
'
,
()
=>
{
beforeEach
(()
=>
{
createComponent
({
stubs
:
{
GlTable
:
GlTableMock
,
},
});
});
it
.
each
([
'
desktop
'
,
'
mobile
'
])(
'
renders the %s edit button
'
,
(
layout
)
=>
{
const
editButton
=
wrapper
.
findByTestId
(
`edit-scan-button-
${
layout
}
`
);
expect
(
editButton
.
exists
()).
toBe
(
true
);
expect
(
editButton
.
attributes
(
'
href
'
)).
toBe
(
firstProfile
.
editPath
);
});
});
describe
(
'
run scan button
'
,
()
=>
{
describe
(
'
success
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createComponent
({
stubs
:
{
GlTable
:
GlTableMock
,
},
});
await
flushPromises
();
});
it
(
'
renders the button
'
,
()
=>
{
expect
(
findRunScanButton
().
exists
()).
toBe
(
true
);
});
it
(
'
clicking on the button triggers the run scan mutation with the profile ID
'
,
()
=>
{
findRunScanButton
().
vm
.
$emit
(
'
click
'
);
expect
(
requestHandlers
.
dastProfileRunMutation
).
toHaveBeenCalledWith
({
input
:
{
id
:
firstProfile
.
id
},
});
});
it
(
'
put the button in the loading and disabled state
'
,
async
()
=>
{
const
runScanButton
=
findRunScanButton
();
runScanButton
.
vm
.
$emit
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
runScanButton
.
props
(
'
loading
'
)).
toBe
(
true
);
expect
(
runScanButton
.
props
(
'
disabled
'
)).
toBe
(
true
);
});
it
(
"
redirects to the pipeline's page once the mutation resolves
"
,
async
()
=>
{
findRunScanButton
().
vm
.
$emit
(
'
click
'
);
await
flushPromises
();
expect
(
redirectTo
).
toHaveBeenCalledWith
(
'
/pipelines/1
'
);
});
});
const
topLevelErrorMessage
=
s__
(
'
OnDemandScans|Could not run the scan. Please try again.
'
);
describe
.
each
`
errorType | errorMessage | requestHander
${
'
error-as-data
'
}
|
${
errorAsDataMessage
}
|
${
jest
.
fn
().
mockResolvedValue
(
makeDastProfileRunResponse
([
errorAsDataMessage
]))}
${
'
top-level error
'
}
|
${
topLevelErrorMessage
}
|
${
jest
.
fn
().
mockRejectedValue
()}
`
(
'
when deletion fails with $errorType
'
,
({
errorMessage
,
requestHander
})
=>
{
beforeEach
(
async
()
=>
{
requestHandlers
.
dastProfileRunMutation
=
requestHander
;
createComponent
({
stubs
:
{
GlTable
:
GlTableMock
,
},
});
await
flushPromises
();
findRunScanButton
().
vm
.
$emit
(
'
click
'
);
});
it
(
'
shows the error message
'
,
()
=>
{
expect
(
wrapper
.
text
()).
toContain
(
errorMessage
);
});
it
(
'
hides the error message when retrying the deletion
'
,
async
()
=>
{
findRunScanButton
().
vm
.
$emit
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
text
()).
not
.
toContain
(
errorMessage
);
});
it
(
"
resets the button's state
"
,
async
()
=>
{
const
runScanButton
=
findRunScanButton
();
expect
(
runScanButton
.
props
(
'
loading
'
)).
toBe
(
false
);
expect
(
runScanButton
.
props
(
'
disabled
'
)).
toBe
(
false
);
});
});
});
describe
(
'
delete button
'
,
()
=>
{
describe
.
each
([
'
desktop
'
,
'
mobile
'
])(
'
%s layout
'
,
(
layout
)
=>
{
let
deleteButton
;
beforeEach
(()
=>
{
createComponent
({
stubs
:
{
GlTable
:
GlTableMock
,
GlModal
:
{
template
:
'
<div />
'
,
methods
:
{
show
:
()
=>
{},
},
},
},
});
deleteButton
=
wrapper
.
findByTestId
(
`delete-scan-button-
${
layout
}
`
);
});
afterEach
(()
=>
{
deleteButton
=
null
;
});
it
(
'
renders the button
'
,
()
=>
{
expect
(
deleteButton
.
exists
()).
toBe
(
true
);
});
it
(
'
clicking on the button opens the delete modal
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
.
$refs
[
'
delete-scan-modal
'
],
'
show
'
);
deleteButton
.
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
vm
.
$refs
[
'
delete-scan-modal
'
].
show
).
toHaveBeenCalled
();
});
it
(
'
confirming the deletion in the modal triggers the delete mutation with the profile ID
'
,
()
=>
{
deleteButton
.
vm
.
$emit
(
'
click
'
);
findDeleteModal
().
vm
.
$emit
(
'
ok
'
);
expect
(
requestHandlers
.
dastProfileDeleteMutation
).
toHaveBeenCalledWith
({
input
:
{
id
:
firstProfile
.
id
},
});
});
});
const
topLevelErrorMessage
=
s__
(
'
OnDemandScans|Could not delete saved scan. Please refresh the page, or try again later.
'
,
);
describe
.
each
`
errorType | errorMessage | requestHander
${
'
error-as-data
'
}
|
${
errorAsDataMessage
}
|
${
jest
.
fn
().
mockResolvedValue
(
makeDastProfileDeleteResponse
([
errorAsDataMessage
]))}
${
'
top-level error
'
}
|
${
topLevelErrorMessage
}
|
${
jest
.
fn
().
mockRejectedValue
()}
`
(
'
when deletion fails with $errorType
'
,
({
errorMessage
,
requestHander
})
=>
{
beforeEach
(
async
()
=>
{
requestHandlers
.
dastProfileDeleteMutation
=
requestHander
;
createComponent
({
stubs
:
{
GlTable
:
GlTableMock
,
},
});
await
flushPromises
();
findDeleteModal
().
vm
.
$emit
(
'
ok
'
);
});
it
(
'
shows the error message
'
,
()
=>
{
expect
(
wrapper
.
text
()).
toContain
(
errorMessage
);
});
it
(
'
hides the error message when retrying the deletion
'
,
async
()
=>
{
findDeleteModal
().
vm
.
$emit
(
'
ok
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
text
()).
not
.
toContain
(
errorMessage
);
});
});
});
});
ee/spec/frontend/on_demand_scans/graphql/cache_utils_spec.js
0 → 100644
View file @
f48d1a32
import
dastProfilesMock
from
'
test_fixtures/graphql/on_demand_scans/graphql/dast_profiles.query.graphql.json
'
;
import
{
removeProfile
}
from
'
ee/on_demand_scans/graphql/cache_utils
'
;
const
[
firstProfile
,
...
otherProfiles
]
=
dastProfilesMock
.
data
.
project
.
pipelines
.
nodes
;
describe
(
'
EE - On-demand Scans GraphQL CacheUtils
'
,
()
=>
{
describe
(
'
removeProfile
'
,
()
=>
{
it
(
'
removes the profile with the given id from the cache
'
,
()
=>
{
const
mockQueryBody
=
{
query
:
'
foo
'
,
variables
:
{
foo
:
'
bar
'
}
};
const
mockStore
=
{
readQuery
:
()
=>
dastProfilesMock
.
data
,
writeQuery
:
jest
.
fn
(),
};
removeProfile
({
store
:
mockStore
,
queryBody
:
mockQueryBody
,
profileId
:
firstProfile
.
id
,
});
expect
(
mockStore
.
writeQuery
).
toHaveBeenCalledWith
({
...
mockQueryBody
,
data
:
{
project
:
{
id
:
dastProfilesMock
.
data
.
project
.
id
,
pipelines
:
{
nodes
:
otherProfiles
,
pageInfo
:
expect
.
any
(
Object
),
},
},
},
});
});
});
});
locale/gitlab.pot
View file @
f48d1a32
...
...
@@ -24516,6 +24516,12 @@ msgstr ""
msgid "OnDemandScans|%{learnMoreLinkStart}Learn more about on-demand scans%{learnMoreLinkEnd}."
msgstr ""
msgid "OnDemandScans|Are you sure you want to delete this scan?"
msgstr ""
msgid "OnDemandScans|Could not delete saved scan. Please refresh the page, or try again later."
msgstr ""
msgid "OnDemandScans|Could not fetch on-demand scans. Please refresh the page, or try again later."
msgstr ""
...
...
@@ -24534,12 +24540,18 @@ msgstr ""
msgid "OnDemandScans|Create new site profile"
msgstr ""
msgid "OnDemandScans|Delete profile"
msgstr ""
msgid "OnDemandScans|Description (optional)"
msgstr ""
msgid "OnDemandScans|Edit on-demand DAST scan"
msgstr ""
msgid "OnDemandScans|Edit profile"
msgstr ""
msgid "OnDemandScans|For example: Tests the login page for SQL injections"
msgstr ""
...
...
@@ -24582,6 +24594,9 @@ msgstr ""
msgid "OnDemandScans|Repeats"
msgstr ""
msgid "OnDemandScans|Run scan"
msgstr ""
msgid "OnDemandScans|Save and run scan"
msgstr ""
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment