Commit 0cf30a73 authored by Paul Gascou-Vaillancourt's avatar Paul Gascou-Vaillancourt Committed by Frédéric Caplette

Add the "Scheduled" tab to the on-demand scans page

This adds the new "Scheduled" tab to the on-demand scans index page.

Changelog: added
EE: true
parent 18fc6343
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module TimeZoneHelper module TimeZoneHelper
TIME_ZONE_FORMAT_ATTRS = { TIME_ZONE_FORMAT_ATTRS = {
short: %i[identifier name offset], short: %i[identifier name offset],
abbr: %i[identifier abbr],
full: %i[identifier name abbr offset formatted_offset] full: %i[identifier name abbr offset formatted_offset]
}.freeze }.freeze
private_constant :TIME_ZONE_FORMAT_ATTRS private_constant :TIME_ZONE_FORMAT_ATTRS
......
...@@ -5791,6 +5791,7 @@ The connection type for [`DastProfile`](#dastprofile). ...@@ -5791,6 +5791,7 @@ The connection type for [`DastProfile`](#dastprofile).
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="dastprofileconnectioncount"></a>`count` | [`Int!`](#int) | Total count of collection. |
| <a id="dastprofileconnectionedges"></a>`edges` | [`[DastProfileEdge]`](#dastprofileedge) | A list of edges. | | <a id="dastprofileconnectionedges"></a>`edges` | [`[DastProfileEdge]`](#dastprofileedge) | A list of edges. |
| <a id="dastprofileconnectionnodes"></a>`nodes` | [`[DastProfile]`](#dastprofile) | A list of nodes. | | <a id="dastprofileconnectionnodes"></a>`nodes` | [`[DastProfile]`](#dastprofile) | A list of nodes. |
| <a id="dastprofileconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | | <a id="dastprofileconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
...@@ -17,6 +17,7 @@ import { ...@@ -17,6 +17,7 @@ import {
import AllTab from './tabs/all.vue'; import AllTab from './tabs/all.vue';
import RunningTab from './tabs/running.vue'; import RunningTab from './tabs/running.vue';
import FinishedTab from './tabs/finished.vue'; import FinishedTab from './tabs/finished.vue';
import ScheduledTab from './tabs/scheduled.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
export default { export default {
...@@ -30,6 +31,7 @@ export default { ...@@ -30,6 +31,7 @@ export default {
AllTab, AllTab,
RunningTab, RunningTab,
FinishedTab, FinishedTab,
ScheduledTab,
EmptyState, EmptyState,
}, },
inject: ['newDastScanPath', 'projectPath', 'projectOnDemandScanCountsEtag'], inject: ['newDastScanPath', 'projectPath', 'projectOnDemandScanCountsEtag'],
...@@ -73,7 +75,10 @@ export default { ...@@ -73,7 +75,10 @@ export default {
return this.liveOnDemandScanCounts ?? this.initialOnDemandScanCounts; return this.liveOnDemandScanCounts ?? this.initialOnDemandScanCounts;
}, },
hasData() { hasData() {
return this.onDemandScanCounts.all > 0; // Scheduled scans aren't included in the total count yet because they are dastProfiles and
// not pipelines. When https://gitlab.com/gitlab-org/gitlab/-/issues/342950 is addressed, we
// will be able to rely on the "all" count only here.
return this.onDemandScanCounts.all + this.onDemandScanCounts.scheduled > 0;
}, },
tabs() { tabs() {
return { return {
...@@ -89,6 +94,10 @@ export default { ...@@ -89,6 +94,10 @@ export default {
component: FinishedTab, component: FinishedTab,
itemsCount: this.onDemandScanCounts.finished, itemsCount: this.onDemandScanCounts.finished,
}, },
scheduled: {
component: ScheduledTab,
itemsCount: this.onDemandScanCounts.scheduled,
},
}; };
}, },
activeTab: { activeTab: {
......
...@@ -223,28 +223,23 @@ export default { ...@@ -223,28 +223,23 @@ export default {
<rect width="70" height="20" x="855" y="5" rx="4" /> <rect width="70" height="20" x="855" y="5" rx="4" />
</gl-skeleton-loader> </gl-skeleton-loader>
</template> </template>
<template #cell(detailedStatus)="{ item }">
<template #cell(status)="{ value }">
<div class="gl-my-3"> <div class="gl-my-3">
<ci-badge-link :status="item.detailedStatus" /> <ci-badge-link :status="value" />
</div> </div>
</template> </template>
<!-- eslint-disable-next-line vue/valid-v-slot --> <template #cell(name)="{ value }">
<template #cell(dastProfile.name)="{ item }"> <gl-truncate v-if="value" :text="value" with-tooltip />
<gl-truncate v-if="item.dastProfile" :text="item.dastProfile.name" with-tooltip />
</template> </template>
<template #cell(scanType)> <template #cell(scanType)>
{{ $options.DAST_SHORT_NAME }} {{ $options.DAST_SHORT_NAME }}
</template> </template>
<!-- eslint-disable-next-line vue/valid-v-slot --> <template #cell(targetUrl)="{ value }">
<template #cell(dastProfile.dastSiteProfile.targetUrl)="{ item }"> <gl-truncate v-if="value" :text="value" with-tooltip />
<gl-truncate
v-if="item.dastProfile"
:text="item.dastProfile.dastSiteProfile.targetUrl"
with-tooltip
/>
</template> </template>
<template #cell(createdAt)="{ item }"> <template #cell(createdAt)="{ item }">
...@@ -258,6 +253,10 @@ export default { ...@@ -258,6 +253,10 @@ export default {
<template #cell(id)="{ item }"> <template #cell(id)="{ item }">
<gl-link :href="item.path">#{{ $options.getIdFromGraphQLId(item.id) }}</gl-link> <gl-link :href="item.path">#{{ $options.getIdFromGraphQLId(item.id) }}</gl-link>
</template> </template>
<template v-for="slot in Object.keys($scopedSlots)" #[slot]="scope">
<slot :name="slot" v-bind="scope"></slot>
</template>
</gl-table> </gl-table>
<div class="gl-display-flex gl-justify-content-center"> <div class="gl-display-flex gl-justify-content-center">
......
<script>
import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import DastScanSchedule from 'ee/security_configuration/dast_profiles/components/dast_scan_schedule.vue';
import scheduledDastProfilesQuery from '../../graphql/scheduled_dast_profiles.query.graphql';
import { SCHEDULED_TAB_TABLE_FIELDS, LEARN_MORE_TEXT } from '../../constants';
import BaseTab from './base_tab.vue';
export default {
query: scheduledDastProfilesQuery,
components: {
GlIcon,
BaseTab,
DastScanSchedule,
},
inject: ['timezones'],
tableFields: SCHEDULED_TAB_TABLE_FIELDS,
i18n: {
title: __('Scheduled'),
emptyStateTitle: s__('OnDemandScans|There are no scheduled scans.'),
emptyStateText: LEARN_MORE_TEXT,
},
methods: {
getTimezoneCode(timezone) {
return this.timezones.find(({ identifier }) => identifier === timezone)?.abbr;
},
},
};
</script>
<template>
<base-tab
:query="$options.query"
:title="$options.i18n.title"
:fields="$options.tableFields"
:empty-state-title="$options.i18n.emptyStateTitle"
:empty-state-text="$options.i18n.emptyStateText"
v-bind="$attrs"
>
<template #cell(nextRun)="{ value: { date, time, timezone } }">
<div class="gl-white-space-nowrap"><gl-icon :size="12" name="calendar" /> {{ date }}</div>
<div class="gl-text-secondary gl-white-space-nowrap">
<gl-icon :size="12" name="clock" /> {{ time }} {{ getTimezoneCode(timezone) }}
</div>
</template>
<template #cell(dastProfileSchedule)="{ value }">
<dast-scan-schedule :schedule="value" />
</template>
</base-tab>
</template>
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import { stripTimezoneFromISODate } from '~/lib/utils/datetime/date_format_utility';
export const HELP_PAGE_PATH = helpPagePath('user/application_security/dast/index', { export const HELP_PAGE_PATH = helpPagePath('user/application_security/dast/index', {
anchor: 'on-demand-scans', anchor: 'on-demand-scans',
...@@ -8,40 +9,98 @@ export const LEARN_MORE_TEXT = s__( ...@@ -8,40 +9,98 @@ export const LEARN_MORE_TEXT = s__(
'OnDemandScans|%{learnMoreLinkStart}Learn more about on-demand scans%{learnMoreLinkEnd}.', 'OnDemandScans|%{learnMoreLinkStart}Learn more about on-demand scans%{learnMoreLinkEnd}.',
); );
export const PIPELINE_TABS_KEYS = ['all', 'running', 'finished']; export const PIPELINE_TABS_KEYS = ['all', 'running', 'finished', 'scheduled'];
export const PIPELINES_PER_PAGE = 20; export const PIPELINES_PER_PAGE = 20;
export const PIPELINES_POLL_INTERVAL = 1000; export const PIPELINES_POLL_INTERVAL = 1000;
export const PIPELINES_COUNT_POLL_INTERVAL = 1000; export const PIPELINES_COUNT_POLL_INTERVAL = 1000;
export const PIPELINES_SCOPE_RUNNING = 'RUNNING'; export const PIPELINES_SCOPE_RUNNING = 'RUNNING';
export const PIPELINES_SCOPE_FINISHED = 'FINISHED'; export const PIPELINES_SCOPE_FINISHED = 'FINISHED';
export const BASE_TABS_TABLE_FIELDS = [ const STATUS_COLUMN = {
{
label: __('Status'), label: __('Status'),
key: 'detailedStatus', key: 'status',
columnClass: 'gl-w-15', columnClass: 'gl-w-15',
}, };
{ const NAME_COLUMN = {
label: __('Name'), label: __('Name'),
key: 'dastProfile.name', key: 'name',
}, };
{ const SCAN_TYPE_COLUMN = {
label: s__('OnDemandScans|Scan type'), label: s__('OnDemandScans|Scan type'),
key: 'scanType', key: 'scanType',
columnClass: 'gl-w-13', columnClass: 'gl-w-13',
}, };
{ const TARGET_COLUMN = {
label: s__('OnDemandScans|Target'), label: s__('OnDemandScans|Target'),
key: 'dastProfile.dastSiteProfile.targetUrl', key: 'targetUrl',
}, };
{ const START_DATE_COLUMN = {
label: __('Start date'), label: __('Start date'),
key: 'createdAt', key: 'createdAt',
columnClass: 'gl-w-15', columnClass: 'gl-w-15',
}, };
{ const PIPELINE_ID_COLUMN = {
label: __('Pipeline'), label: __('Pipeline'),
key: 'id', key: 'id',
columnClass: 'gl-w-13', columnClass: 'gl-w-13',
};
export const BASE_TABS_TABLE_FIELDS = [
{
...STATUS_COLUMN,
formatter: (_value, _key, item) => item.detailedStatus,
},
{
...NAME_COLUMN,
formatter: (_value, _key, item) => item.dastProfile.name,
},
SCAN_TYPE_COLUMN,
{
...TARGET_COLUMN,
formatter: (_value, _key, item) => item.dastProfile.dastSiteProfile.targetUrl,
},
START_DATE_COLUMN,
PIPELINE_ID_COLUMN,
];
export const SCHEDULED_TAB_TABLE_FIELDS = [
{
...STATUS_COLUMN,
formatter: (_value, _key, item) => ({
detailsPath: item.editPath,
text: __('Scheduled'),
icon: 'status_scheduled',
group: 'scheduled',
}),
},
NAME_COLUMN,
SCAN_TYPE_COLUMN,
{
...TARGET_COLUMN,
formatter: (_value, _key, item) => item.dastSiteProfile.targetUrl,
},
{
label: __('Next scan'),
key: 'nextRun',
formatter: (_value, _key, item) => {
const date = new Date(item.dastProfileSchedule.nextRunAt);
const time = new Date(stripTimezoneFromISODate(item.dastProfileSchedule.startsAt));
return {
date: date.toLocaleDateString(window.navigator.language, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
}),
time: time.toLocaleTimeString(window.navigator.language, {
hour: '2-digit',
minute: '2-digit',
}),
timezone: item.dastProfileSchedule.timezone,
};
},
},
{
label: s__('OnDemandScans|Repeats'),
key: 'dastProfileSchedule',
}, },
]; ];
...@@ -21,4 +21,10 @@ query onDemandScanCounts( ...@@ -21,4 +21,10 @@ query onDemandScanCounts(
count count
} }
} }
scheduled: project(fullPath: $fullPath) {
id
pipelines: dastProfiles(hasDastProfileSchedule: true) {
count
}
}
} }
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query ScheduledDastProfiles(
$fullPath: ID!
$after: String
$before: String
$first: Int
$last: Int
) {
project(fullPath: $fullPath) {
id
pipelines: dastProfiles(
after: $after
before: $before
first: $first
last: $last
hasDastProfileSchedule: true
) {
pageInfo {
...PageInfo
}
nodes {
id
name
dastSiteProfile {
id
targetUrl
}
dastProfileSchedule {
id
active
nextRunAt
startsAt
timezone
cadence {
unit
duration
}
}
editPath
}
}
}
}
...@@ -15,6 +15,7 @@ export default () => { ...@@ -15,6 +15,7 @@ export default () => {
projectOnDemandScanCountsEtag, projectOnDemandScanCountsEtag,
} = el.dataset; } = el.dataset;
const initialOnDemandScanCounts = JSON.parse(el.dataset.onDemandScanCounts); const initialOnDemandScanCounts = JSON.parse(el.dataset.onDemandScanCounts);
const timezones = JSON.parse(el.dataset.timezones);
return new Vue({ return new Vue({
el, el,
...@@ -25,6 +26,7 @@ export default () => { ...@@ -25,6 +26,7 @@ export default () => {
newDastScanPath, newDastScanPath,
emptyStateSvgPath, emptyStateSvgPath,
projectOnDemandScanCountsEtag, projectOnDemandScanCountsEtag,
timezones,
}, },
render(h) { render(h) {
return h(OnDemandScans, { return h(OnDemandScans, {
......
...@@ -6,6 +6,8 @@ module Types ...@@ -6,6 +6,8 @@ module Types
graphql_name 'DastProfile' graphql_name 'DastProfile'
description 'Represents a DAST Profile' description 'Represents a DAST Profile'
connection_type_class(Types::CountableConnectionType)
authorize :read_on_demand_dast_scan authorize :read_on_demand_dast_scan
field :id, ::Types::GlobalIDType[::Dast::Profile], null: false, field :id, ::Types::GlobalIDType[::Dast::Profile], null: false,
......
...@@ -5,16 +5,19 @@ module Projects::OnDemandScansHelper ...@@ -5,16 +5,19 @@ module Projects::OnDemandScansHelper
def on_demand_scans_data(project) def on_demand_scans_data(project)
on_demand_scans = project.all_pipelines.where(source: Enums::Ci::Pipeline.sources[:ondemand_dast_scan]) on_demand_scans = project.all_pipelines.where(source: Enums::Ci::Pipeline.sources[:ondemand_dast_scan])
running_scan_count, finished_scan_count = count_running_and_finished_scans(on_demand_scans) running_scan_count, finished_scan_count = count_running_and_finished_scans(on_demand_scans)
scheduled_scans = ::Dast::ProfilesFinder.new({ project_id: project.id, has_dast_profile_schedule: true }).execute
common_data(project).merge({ common_data(project).merge({
'project-on-demand-scan-counts-etag' => graphql_etag_project_on_demand_scan_counts_path(project), 'project-on-demand-scan-counts-etag' => graphql_etag_project_on_demand_scan_counts_path(project),
'on-demand-scan-counts' => { 'on-demand-scan-counts' => {
all: on_demand_scans.length, all: on_demand_scans.length,
running: running_scan_count, running: running_scan_count,
finished: finished_scan_count finished: finished_scan_count,
scheduled: scheduled_scans.length
}.to_json, }.to_json,
'new-dast-scan-path' => new_project_on_demand_scan_path(project), 'new-dast-scan-path' => new_project_on_demand_scan_path(project),
'empty-state-svg-path' => image_path('illustrations/empty-state/ondemand-scan-empty.svg') 'empty-state-svg-path' => image_path('illustrations/empty-state/ondemand-scan-empty.svg'),
'timezones' => timezone_data(format: :abbr).to_json
}) })
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -7,7 +7,7 @@ module Projects::Security::DastProfilesHelper ...@@ -7,7 +7,7 @@ module Projects::Security::DastProfilesHelper
'new_dast_site_profile_path' => new_project_security_configuration_dast_scans_dast_site_profile_path(project), 'new_dast_site_profile_path' => new_project_security_configuration_dast_scans_dast_site_profile_path(project),
'new_dast_scanner_profile_path' => new_project_security_configuration_dast_scans_dast_scanner_profile_path(project), 'new_dast_scanner_profile_path' => new_project_security_configuration_dast_scans_dast_scanner_profile_path(project),
'project_full_path' => project.path_with_namespace, 'project_full_path' => project.path_with_namespace,
'timezones' => timezone_data(format: :full).to_json 'timezones' => timezone_data(format: :abbr).to_json
} }
end end
end end
...@@ -160,5 +160,24 @@ RSpec.describe 'DAST profiles (GraphQL fixtures)' do ...@@ -160,5 +160,24 @@ RSpec.describe 'DAST profiles (GraphQL fixtures)' do
end end
end end
end end
describe 'scheduled_dast_profiles' do
path = 'on_demand_scans/graphql/scheduled_dast_profiles.query.graphql'
let_it_be(:dast_profile) { create(:dast_profile, project: project) }
let_it_be(:dast_profile_schedule) { create(:dast_profile_schedule, project: project, dast_profile: dast_profile)}
it "graphql/#{path}.json" do
query = get_graphql_query_as_string(path, ee: true)
post_graphql(query, current_user: current_user, variables: {
fullPath: project.full_path,
first: 20
})
expect_graphql_errors_to_be_empty
expect(graphql_data_at(:project, :pipelines, :nodes)).to have_attributes(size: 1)
end
end
end end
end end
...@@ -29,6 +29,7 @@ describe('OnDemandScans', () => { ...@@ -29,6 +29,7 @@ describe('OnDemandScans', () => {
all: 12, all: 12,
running: 3, running: 3,
finished: 9, finished: 9,
scheduled: 5,
}; };
const emptyInitialPipelineCounts = Object.fromEntries(PIPELINE_TABS_KEYS.map((key) => [key, 0])); const emptyInitialPipelineCounts = Object.fromEntries(PIPELINE_TABS_KEYS.map((key) => [key, 0]));
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { merge } from 'lodash';
import scheduledDastProfilesMock from 'test_fixtures/graphql/on_demand_scans/graphql/scheduled_dast_profiles.query.graphql.json';
import mockTimezones from 'test_fixtures/timezones/abbr.json';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ScheduledTab from 'ee/on_demand_scans/components/tabs/scheduled.vue';
import BaseTab from 'ee/on_demand_scans/components/tabs/base_tab.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import scheduledDastProfilesQuery from 'ee/on_demand_scans/graphql/scheduled_dast_profiles.query.graphql';
import { createRouter } from 'ee/on_demand_scans/router';
import { SCHEDULED_TAB_TABLE_FIELDS, LEARN_MORE_TEXT } from 'ee/on_demand_scans/constants';
import { __, s__ } from '~/locale';
import { stripTimezoneFromISODate } from '~/lib/utils/datetime/date_format_utility';
import DastScanSchedule from 'ee/security_configuration/dast_profiles/components/dast_scan_schedule.vue';
jest.mock('~/lib/utils/common_utils');
Vue.use(VueApollo);
describe('Scheduled tab', () => {
let wrapper;
let router;
let requestHandler;
// Props
const projectPath = '/namespace/project';
const itemsCount = 12;
// Finders
const findBaseTab = () => wrapper.findComponent(BaseTab);
const findFirstRow = () => wrapper.find('tbody > tr');
const findCellAt = (index) => findFirstRow().findAll('td').at(index);
// Helpers
const createMockApolloProvider = () => {
return createMockApollo([[scheduledDastProfilesQuery, requestHandler]]);
};
const createComponentFactory = (mountFn = shallowMountExtended) => (options = {}) => {
router = createRouter();
wrapper = mountFn(
ScheduledTab,
merge(
{
apolloProvider: createMockApolloProvider(),
router,
propsData: {
isActive: true,
itemsCount,
},
provide: {
projectPath,
timezones: mockTimezones,
},
stubs: {
BaseTab,
},
},
options,
),
);
};
const createComponent = createComponentFactory();
const createFullComponent = createComponentFactory(mountExtended);
beforeEach(() => {
requestHandler = jest.fn().mockResolvedValue(scheduledDastProfilesMock);
});
afterEach(() => {
wrapper.destroy();
router = null;
requestHandler = null;
});
it('renders the base tab with the correct props', () => {
createComponent();
expect(findBaseTab().props('title')).toBe(__('Scheduled'));
expect(findBaseTab().props('itemsCount')).toBe(itemsCount);
expect(findBaseTab().props('query')).toBe(scheduledDastProfilesQuery);
expect(findBaseTab().props('emptyStateTitle')).toBe(
s__('OnDemandScans|There are no scheduled scans.'),
);
expect(findBaseTab().props('emptyStateText')).toBe(LEARN_MORE_TEXT);
expect(findBaseTab().props('fields')).toBe(SCHEDULED_TAB_TABLE_FIELDS);
});
it('fetches the profiles', () => {
createComponent();
expect(requestHandler).toHaveBeenCalledWith({
after: null,
before: null,
first: 20,
fullPath: projectPath,
last: null,
});
});
describe('custom table cells', () => {
const [firstProfile] = scheduledDastProfilesMock.data.project.pipelines.nodes;
beforeEach(() => {
createFullComponent();
});
it('renders the next run cell', () => {
const nextRunCell = findCellAt(4);
expect(nextRunCell.text()).toContain(
new Date(firstProfile.dastProfileSchedule.nextRunAt).toLocaleDateString(
window.navigator.language,
{
year: 'numeric',
month: 'numeric',
day: 'numeric',
},
),
);
expect(nextRunCell.text()).toContain(
new Date(
stripTimezoneFromISODate(firstProfile.dastProfileSchedule.startsAt),
).toLocaleTimeString(window.navigator.language, {
hour: '2-digit',
minute: '2-digit',
}),
);
});
it('renders the schedule cell', () => {
const scheduleCell = findCellAt(5);
const dastScanScheduleComponent = scheduleCell.find(DastScanSchedule);
expect(dastScanScheduleComponent.exists()).toBe(true);
expect(dastScanScheduleComponent.props('schedule')).toEqual(firstProfile.dastProfileSchedule);
});
});
});
...@@ -6,13 +6,18 @@ RSpec.describe Projects::OnDemandScansHelper do ...@@ -6,13 +6,18 @@ RSpec.describe Projects::OnDemandScansHelper do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:path_with_namespace) { "foo/bar" } let_it_be(:path_with_namespace) { "foo/bar" }
let_it_be(:graphql_etag_project_on_demand_scan_counts_path) {"/api/graphql:#{path_with_namespace}/on_demand_scans/counts" } let_it_be(:graphql_etag_project_on_demand_scan_counts_path) {"/api/graphql:#{path_with_namespace}/on_demand_scans/counts" }
let_it_be(:timezones) { [{ identifier: "Europe/Paris" }] }
before do before do
allow(project).to receive(:path_with_namespace).and_return(path_with_namespace) allow(project).to receive(:path_with_namespace).and_return(path_with_namespace)
end end
describe '#on_demand_scans_data' do describe '#on_demand_scans_data' do
let_it_be(:dast_profile) { create(:dast_profile, project: project) }
let_it_be(:dast_profile_schedule) { create(:dast_profile_schedule, project: project, dast_profile: dast_profile)}
before do before do
allow(helper).to receive(:timezone_data).with(format: :abbr).and_return(timezones)
create_list(:ci_pipeline, 8, :success, project: project, ref: 'master', source: :ondemand_dast_scan) create_list(:ci_pipeline, 8, :success, project: project, ref: 'master', source: :ondemand_dast_scan)
create_list(:ci_pipeline, 4, :running, project: project, ref: 'master', source: :ondemand_dast_scan) create_list(:ci_pipeline, 4, :running, project: project, ref: 'master', source: :ondemand_dast_scan)
allow(helper).to receive(:graphql_etag_project_on_demand_scan_counts_path).and_return(graphql_etag_project_on_demand_scan_counts_path) allow(helper).to receive(:graphql_etag_project_on_demand_scan_counts_path).and_return(graphql_etag_project_on_demand_scan_counts_path)
...@@ -27,18 +32,18 @@ RSpec.describe Projects::OnDemandScansHelper do ...@@ -27,18 +32,18 @@ RSpec.describe Projects::OnDemandScansHelper do
'on-demand-scan-counts' => { 'on-demand-scan-counts' => {
all: 12, all: 12,
running: 4, running: 4,
finished: 8 finished: 8,
}.to_json scheduled: 1
}.to_json,
'timezones' => timezones.to_json
) )
end end
end end
describe '#on_demand_scans_form_data' do describe '#on_demand_scans_form_data' do
let_it_be(:timezones) { [{ identifier: "Europe/Paris" }] }
before do before do
allow(project).to receive(:default_branch).and_return("default-branch")
allow(helper).to receive(:timezone_data).with(format: :full).and_return(timezones) allow(helper).to receive(:timezone_data).with(format: :full).and_return(timezones)
allow(project).to receive(:default_branch).and_return("default-branch")
end end
it 'returns proper data' do it 'returns proper data' do
......
...@@ -9,7 +9,7 @@ RSpec.describe Projects::Security::DastProfilesHelper do ...@@ -9,7 +9,7 @@ RSpec.describe Projects::Security::DastProfilesHelper do
before do before do
allow(project).to receive(:path_with_namespace).and_return("foo/bar") allow(project).to receive(:path_with_namespace).and_return("foo/bar")
allow(helper).to receive(:timezone_data).with(format: :full).and_return(timezones) allow(helper).to receive(:timezone_data).with(format: :abbr).and_return(timezones)
end end
it 'returns proper data' do it 'returns proper data' do
......
...@@ -23433,6 +23433,9 @@ msgstr "" ...@@ -23433,6 +23433,9 @@ msgstr ""
msgid "Next file in diff" msgid "Next file in diff"
msgstr "" msgstr ""
msgid "Next scan"
msgstr ""
msgid "Next unresolved discussion" msgid "Next unresolved discussion"
msgstr "" msgstr ""
...@@ -24331,6 +24334,9 @@ msgstr "" ...@@ -24331,6 +24334,9 @@ msgstr ""
msgid "OnDemandScans|On-demand scans run outside the DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Learn more%{learnMoreLinkEnd}" msgid "OnDemandScans|On-demand scans run outside the DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Learn more%{learnMoreLinkEnd}"
msgstr "" msgstr ""
msgid "OnDemandScans|Repeats"
msgstr ""
msgid "OnDemandScans|Save and run scan" msgid "OnDemandScans|Save and run scan"
msgstr "" msgstr ""
...@@ -24367,6 +24373,9 @@ msgstr "" ...@@ -24367,6 +24373,9 @@ msgstr ""
msgid "OnDemandScans|There are no running scans." msgid "OnDemandScans|There are no running scans."
msgstr "" msgstr ""
msgid "OnDemandScans|There are no scheduled scans."
msgstr ""
msgid "OnDemandScans|Use existing scanner profile" msgid "OnDemandScans|Use existing scanner profile"
msgstr "" msgstr ""
......
...@@ -8,11 +8,9 @@ RSpec.describe TimeZoneHelper, '(JavaScript fixtures)' do ...@@ -8,11 +8,9 @@ RSpec.describe TimeZoneHelper, '(JavaScript fixtures)' do
let(:response) { @timezones.sort_by! { |tz| tz[:name] }.to_json } let(:response) { @timezones.sort_by! { |tz| tz[:name] }.to_json }
it 'timezones/short.json' do %I[short abbr full].each do |format|
@timezones = timezone_data(format: :short) it "timezones/#{format}.json" do
@timezones = timezone_data(format: format)
end end
it 'timezones/full.json' do
@timezones = timezone_data(format: :full)
end end
end end
...@@ -30,6 +30,30 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do ...@@ -30,6 +30,30 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do
end end
end end
context 'with abbr format' do
subject(:timezone_data) { helper.timezone_data(format: :abbr) }
it 'matches schema' do
expect(timezone_data).not_to be_empty
timezone_data.each_with_index do |timezone_hash, i|
expect(timezone_hash.keys).to contain_exactly(
:identifier,
:abbr
), "Failed at index #{i}"
end
end
it 'formats for display' do
tz = ActiveSupport::TimeZone.all[0]
expect(timezone_data[0]).to eq(
identifier: tz.tzinfo.identifier,
abbr: tz.tzinfo.strftime('%Z')
)
end
end
context 'with full format' do context 'with full format' do
subject(:timezone_data) { helper.timezone_data(format: :full) } subject(:timezone_data) { helper.timezone_data(format: :full) }
...@@ -64,7 +88,7 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do ...@@ -64,7 +88,7 @@ RSpec.describe TimeZoneHelper, :aggregate_failures do
subject(:timezone_data) { helper.timezone_data(format: :unknown) } subject(:timezone_data) { helper.timezone_data(format: :unknown) }
it 'raises an exception' do it 'raises an exception' do
expect { timezone_data }.to raise_error ArgumentError, 'Invalid format :unknown. Valid formats are :short, :full.' expect { timezone_data }.to raise_error ArgumentError, 'Invalid format :unknown. Valid formats are :short, :abbr, :full.'
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment