Commit 5541e08b authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 977eef18 0f642b99
......@@ -38,7 +38,7 @@ module Security
def execute
return [] if @job_types.empty?
if Feature.enabled?(:ci_build_metadata_config)
if Feature.enabled?(:ci_build_metadata_config, pipeline.project, default_enabled: :yaml)
find_jobs
else
find_jobs_legacy
......
......@@ -22,8 +22,8 @@ module Ci
validates :build, presence: true
validates :secrets, json_schema: { filename: 'build_metadata_secrets' }
serialize :config_options, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
serialize :config_variables, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
serialize :config_options, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
serialize :config_variables, Serializers::SymbolizedJson # rubocop:disable Cop/ActiveRecordSerialize
chronic_duration_attr_reader :timeout_human_readable, :timeout
......
......@@ -77,7 +77,7 @@ module Ci
def write_metadata_attribute(legacy_key, metadata_key, value)
# save to metadata or this model depending on the state of feature flag
if Feature.enabled?(:ci_build_metadata_config)
if Feature.enabled?(:ci_build_metadata_config, project, default_enabled: :yaml)
ensure_metadata.write_attribute(metadata_key, value)
write_attribute(legacy_key, nil)
else
......
......@@ -5,4 +5,4 @@ rollout_issue_url:
milestone: '14.0'
type: development
group: group::source code
default_enabled: false
default_enabled: true
---
name: ci_build_metadata_config
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/7238
rollout_issue_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330954
milestone: '11.7'
type: development
group: group::pipeline execution
......
---
name: erase_traces_from_already_archived_jobs_when_archiving_again
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56353
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326679
milestone: "13.11"
type: development
group: group::pipeline execution
default_enabled: true
---
name: sec_secret_detection_ui_enable
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57869
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326049
milestone: '13.11'
name: jira_issue_details_edit_status
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60092
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330628
milestone: '14.1'
type: development
group: group::static analysis
default_enabled: true
group: group::ecosystem
default_enabled: false
---
name: use_traversal_ids_for_ancestors
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57137
rollout_issue_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334952
milestone: '13.12'
type: development
group: group::access
......
# frozen_string_literal: true
class ScheduleDeleteOrphanedDeployments < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'DeleteOrphanedDeployments'
BATCH_SIZE = 100_000
DELAY_INTERVAL = 2.minutes
disable_ddl_transaction!
def up
queue_background_migration_jobs_by_range_at_intervals(
define_batchable_model('deployments'),
MIGRATION,
DELAY_INTERVAL,
batch_size: BATCH_SIZE,
track_jobs: true
)
end
def down
# no-op
end
end
432954295d6f3a2a45f3deef42b547ffe42501beaea4f376e1be51cf148de671
\ No newline at end of file
......@@ -139,18 +139,8 @@ always take the latest Secret Detection artifact available.
### Enable Secret Detection via an automatic merge request **(ULTIMATE SELF)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4496) in GitLab 13.11.
> - [Deployed behind a feature flag](../../../user/feature_flags.md), enabled by default.
> - Enabled on GitLab.com.
> - Recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-configure-secret-detection-via-a-merge-request). **(ULTIMATE SELF)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
There can be
[risks when disabling released features](../../../user/feature_flags.md#risks-when-disabling-released-features).
Refer to this feature's version history for more details.
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4496) in GitLab 13.11, behind a feature flag, enabled by default.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/329886) in GitLab 14.1.
To enable Secret Detection in a project, you can create a merge request
from the Security Configuration page.
......@@ -409,22 +399,3 @@ secret_detection:
variables:
GIT_DEPTH: 100
```
### Enable or disable Configure Secret Detection via a Merge Request
Configure Secret Detection via a Merge Request is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can opt to disable it.
To enable it:
```ruby
Feature.enable(:sec_secret_detection_ui_enable)
```
To disable it:
```ruby
Feature.disable(:sec_secret_detection_ui_enable)
```
......@@ -48,6 +48,10 @@ Iteration cadences automate some common iteration tasks. They can be used to
automatically create iterations every 1, 2, 3, 4, or 6 weeks. They can also
be configured to automatically roll over incomplete issues to the next iteration.
With iteration cadences enabled, you must first
[create an iteration cadence](#create-an-iteration-cadence) before you can
[create an iteration](#create-an-iteration).
### Create an iteration cadence
Prerequisites:
......@@ -94,7 +98,7 @@ To create an iteration:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Issues > Iterations**.
1. Select the three-dot menu (**{ellipsis_v}**) > **Add iteration** for the cadence you want to add to.
1. Select **New iteration**.
1. Enter the title, a description (optional), a start date, and a due date.
1. Select **Create iteration**. The iteration details page opens.
......@@ -191,13 +195,13 @@ can enable it.
To enable it:
```ruby
Feature.enable(:iterations_cadences)
Feature.enable(:iteration_cadences)
```
To disable it:
```ruby
Feature.disable(:iterations_cadences)
Feature.disable(:iteration_cadences)
```
<!-- ## Troubleshooting
......
......@@ -5,3 +5,22 @@ export const fetchIssue = async (issuePath) => {
return data;
});
};
export const fetchIssueStatuses = () => {
// We are using mock data here which should come from the backend
return new Promise((resolve) => {
setTimeout(() => {
// eslint-disable-next-line @gitlab/require-i18n-strings
resolve([{ title: 'In Progress' }, { title: 'Done' }]);
}, 1000);
});
};
export const updateIssue = (issue, { status }) => {
// We are using mock call here which should become a backend call
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ...issue, status });
}, 1000);
});
};
......@@ -7,9 +7,11 @@ import {
GlBadge,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
import { fetchIssue } from 'ee/integrations/jira/issues_show/api';
import { fetchIssue, fetchIssueStatuses, updateIssue } from 'ee/integrations/jira/issues_show/api';
import JiraIssueSidebar from 'ee/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue';
import { issueStates, issueStateLabels } from 'ee/integrations/jira/issues_show/constants';
import createFlash from '~/flash';
import IssuableShow from '~/issuable_show/components/issuable_show_root.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
......@@ -38,8 +40,11 @@ export default {
data() {
return {
isLoading: true,
isLoadingStatus: false,
isUpdatingStatus: false,
errorMessage: null,
issue: {},
statuses: [],
};
},
computed: {
......@@ -78,6 +83,41 @@ export default {
jiraIssueCommentId(id) {
return `jira_note_${id}`;
},
onIssueStatusFetch() {
this.isLoadingStatus = true;
fetchIssueStatuses()
.then((response) => {
this.statuses = response;
})
.catch(() => {
createFlash({
message: s__(
'JiraService|Failed to load Jira issue statuses. View the issue in Jira, or reload the page.',
),
});
})
.finally(() => {
this.isLoadingStatus = false;
});
},
onIssueStatusUpdated(status) {
this.isUpdatingStatus = true;
updateIssue(this.issue, { status })
.then(() => {
this.issue = { ...this.issue, status };
})
.catch(() => {
createFlash({
message: s__(
'JiraService|Failed to update Jira issue status. View the issue in Jira, or reload the page.',
),
});
})
.finally(() => {
this.isUpdatingStatus = false;
});
},
},
};
</script>
......@@ -117,7 +157,15 @@ export default {
<template #status-badge>{{ statusBadgeText }}</template>
<template #right-sidebar-items="{ sidebarExpanded }">
<jira-issue-sidebar :sidebar-expanded="sidebarExpanded" :issue="issue" />
<jira-issue-sidebar
:sidebar-expanded="sidebarExpanded"
:issue="issue"
:is-loading-status="isLoadingStatus"
:is-updating-status="isUpdatingStatus"
:statuses="statuses"
@issue-status-fetch="onIssueStatusFetch"
@issue-status-updated="onIssueStatusUpdated"
/>
</template>
<template #discussion>
......
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import IssueFieldDropdown from './issue_field_dropdown.vue';
export default {
directives: {
......@@ -8,12 +10,50 @@ export default {
},
components: {
GlIcon,
IssueFieldDropdown,
SidebarEditableItem,
},
provide() {
return {
isClassicSidebar: true,
canUpdate: this.canUpdate,
};
},
props: {
canUpdate: {
type: Boolean,
required: false,
default: false,
},
dropdownEmpty: {
type: String,
required: false,
default: null,
},
dropdownTitle: {
type: String,
required: false,
default: null,
},
icon: {
type: String,
required: true,
},
items: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
updating: {
type: Boolean,
required: false,
default: false,
},
title: {
type: String,
required: true,
......@@ -44,20 +84,58 @@ export default {
i18n: {
none: __('None'),
},
methods: {
showDropdown() {
this.$refs.dropdown.showDropdown();
this.$emit('issue-field-fetch');
},
expandSidebarAndOpenDropdown() {
this.$emit('expand-sidebar', this.$refs.editableItem);
},
onIssueFieldUpdated(value) {
this.$emit('issue-field-updated', value);
},
},
};
</script>
<template>
<div class="block">
<div v-gl-tooltip="tooltipProps" class="sidebar-collapsed-icon" data-testid="field-collapsed">
<gl-icon :name="icon" />
</div>
<sidebar-editable-item
ref="editableItem"
:loading="updating"
:title="title"
@open="showDropdown"
>
<template #collapsed>
<div
v-gl-tooltip="tooltipProps"
class="sidebar-collapsed-icon"
data-testid="field-collapsed"
@click="expandSidebarAndOpenDropdown"
>
<gl-icon :name="icon" />
</div>
<div class="hide-collapsed">
<div class="value" data-testid="field-value">
<span :class="valueClass">{{ valueWithFallback }}</span>
</div>
</div>
</template>
<div class="hide-collapsed">
<div class="title" data-testid="field-title">{{ title }}</div>
<div class="value">
<span :class="valueClass" data-testid="field-value">{{ valueWithFallback }}</span>
</div>
</div>
<template #default>
<issue-field-dropdown
v-if="canUpdate"
ref="dropdown"
:empty-text="dropdownEmpty"
:items="items"
:loading="loading"
:text="valueWithFallback"
:title="dropdownTitle"
@issue-field-updated="onIssueFieldUpdated"
/>
</template>
</sidebar-editable-item>
</div>
</template>
<script>
import { GlDropdown, GlDropdownItem, GlDropdownText, GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlLoadingIcon,
},
props: {
emptyText: {
type: String,
required: false,
default: null,
},
items: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: true,
},
text: {
type: String,
required: false,
default: null,
},
title: {
type: String,
required: false,
default: null,
},
},
computed: {
noItems() {
return this.items.length === 0;
},
},
methods: {
showDropdown() {
this.$refs.dropdown.show();
},
selectItem(item) {
this.$emit('issue-field-updated', item.title);
},
},
};
</script>
<template>
<gl-dropdown ref="dropdown" :text="text" :header-text="title" block lazy>
<div v-if="loading" class="gl-h-13">
<gl-loading-icon size="md" />
</div>
<div v-else>
<gl-dropdown-text v-if="noItems">{{ emptyText }}</gl-dropdown-text>
<gl-dropdown-item v-for="item in items" :key="item.title" @click="selectItem(item)">
{{ item.title }}
</gl-dropdown-item>
</div>
</gl-dropdown>
</template>
<script>
import { labelsFilterParam } from 'ee/integrations/jira/issues_show/constants';
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Assignee from './assignee.vue';
import IssueDueDate from './issue_due_date.vue';
import IssueField from './issue_field.vue';
......@@ -16,6 +17,7 @@ export default {
CopyableField,
LabelsSelect,
},
mixins: [glFeatureFlagsMixin()],
inject: {
issuesListPath: {
default: null,
......@@ -30,6 +32,21 @@ export default {
type: Object,
required: true,
},
isLoadingStatus: {
type: Boolean,
required: false,
default: false,
},
isUpdatingStatus: {
type: Boolean,
required: false,
default: false,
},
statuses: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
assignee() {
......@@ -39,12 +56,50 @@ export default {
reference() {
return this.issue.references?.relative;
},
canUpdateStatus() {
return this.glFeatures.jiraIssueDetailsEditStatus;
},
},
labelsFilterParam,
i18n: {
statusTitle: __('Status'),
statusDropdownEmpty: s__('JiraService|No available statuses'),
statusDropdownTitle: __('Change status'),
referenceName: __('Reference'),
},
mounted() {
this.sidebarEl = document.querySelector('aside.right-sidebar');
this.sidebarToggleEl = document.querySelector('.js-toggle-right-sidebar-button');
},
methods: {
toggleSidebar() {
this.sidebarToggleEl.dispatchEvent(new Event('click'));
},
expandSidebarAndOpenDropdown(dropdownRef = null) {
// Expand the sidebar if not already expanded.
if (!this.sidebarExpanded) {
this.toggleSidebar();
}
if (dropdownRef) {
// Wait for sidebar expand animation to complete
// before revealing the dropdown.
this.sidebarEl.addEventListener(
'transitionend',
() => {
dropdownRef.expand();
},
{ once: true },
);
}
},
onIssueStatusFetch() {
this.$emit('issue-status-fetch');
},
onIssueStatusUpdated(status) {
this.$emit('issue-status-updated', status);
},
},
};
</script>
......@@ -52,7 +107,20 @@ export default {
<div>
<assignee class="block" :assignee="assignee" />
<issue-due-date :due-date="issue.dueDate" />
<issue-field icon="progress" :title="$options.i18n.statusTitle" :value="issue.status" />
<issue-field
icon="progress"
:can-update="canUpdateStatus"
:dropdown-title="$options.i18n.statusDropdownTitle"
:dropdown-empty="$options.i18n.statusDropdownEmpty"
:items="statuses"
:loading="isLoadingStatus"
:title="$options.i18n.statusTitle"
:updating="isUpdatingStatus"
:value="issue.status"
@expand-sidebar="expandSidebarAndOpenDropdown"
@issue-field-fetch="onIssueStatusFetch"
@issue-field-updated="onIssueStatusUpdated"
/>
<labels-select
:selected-labels="issue.labels"
:labels-filter-base-path="issuesListPath"
......
......@@ -9,6 +9,7 @@ query IterationCadences(
) {
group(fullPath: $fullPath) {
iterationCadences(
includeAncestorGroups: true
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
......
......@@ -25,9 +25,6 @@ export default {
if (!this.glFeatures.secDependencyScanningUiEnable) {
delete scannerComponentMapCopy[REPORT_TYPE_DEPENDENCY_SCANNING];
}
if (!this.glFeatures.secSecretDetectionUiEnable) {
delete scannerComponentMapCopy[REPORT_TYPE_SECRET_DETECTION];
}
return scannerComponentMapCopy;
},
manageComponent() {
......
......@@ -15,7 +15,6 @@ module EE
before_action only: [:show] do
push_frontend_feature_flag(:security_auto_fix, project, default_enabled: false)
push_frontend_feature_flag(:sec_dependency_scanning_ui_enable, project, default_enabled: :yaml)
push_frontend_feature_flag(:sec_secret_detection_ui_enable, project, default_enabled: :yaml)
push_frontend_feature_flag(:dast_configuration_ui, project, default_enabled: :yaml)
end
......
......@@ -13,6 +13,9 @@ module Projects
name: 'i_ecosystem_jira_service_list_issues'
before_action :check_feature_enabled!
before_action only: :show do
push_frontend_feature_flag(:jira_issue_details_edit_status, project, default_enabled: :yaml)
end
rescue_from ::Projects::Integrations::Jira::IssuesFinder::IntegrationError, with: :render_integration_error
rescue_from ::Projects::Integrations::Jira::IssuesFinder::RequestError, with: :render_request_error
......
......@@ -11,13 +11,9 @@ module LatestPipelineInformation
strong_memoize("latest_builds_reports_#{only_successful_builds}" ) do
builds = latest_security_builds
builds = builds.select { |build| build.status == 'success' } if only_successful_builds
builds.map do |build|
if Feature.enabled?(:ci_build_metadata_config)
build.metadata.config_options[:artifacts][:reports].keys.map(&:to_sym)
else
build.options[:artifacts][:reports].keys
end
end.flatten
builds.flat_map do |build|
build.options[:artifacts][:reports].keys
end
end
end
......
......@@ -5,19 +5,21 @@ require 'spec_helper'
RSpec.describe 'User views iteration cadences', :js do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:cadence) { create(:iterations_cadence, group: group) }
let_it_be(:other_cadence) { create(:iterations_cadence, group: group) }
let_it_be(:subgroup_cadence) { create(:iterations_cadence, group: subgroup) }
let_it_be(:iteration_in_cadence) { create(:iteration, group: group, iterations_cadence: cadence) }
let_it_be(:closed_iteration_in_cadence) { create(:iteration, group: group, iterations_cadence: cadence, start_date: 2.weeks.ago, due_date: 1.week.ago) }
let_it_be(:iteration_in_other_cadence) { create(:iteration, group: group, iterations_cadence: other_cadence) }
before do
stub_licensed_features(iterations: true)
visit group_iteration_cadences_path(group)
end
it 'shows iteration cadences with iterations when expanded', :aggregate_failures do
visit group_iteration_cadences_path(group)
expect(page).to have_title('Iteration cadences')
expect(page).to have_content(cadence.title)
expect(page).to have_content(other_cadence.title)
......@@ -27,15 +29,25 @@ RSpec.describe 'User views iteration cadences', :js do
click_button cadence.title
expect(page).to have_content(iteration_in_cadence.title)
expect(page).not_to have_content(subgroup_cadence.title)
expect(page).not_to have_content(iteration_in_other_cadence.title)
expect(page).not_to have_content(closed_iteration_in_cadence.title)
end
it 'only shows completed iterations on Done tab', :aggregate_failures do
visit group_iteration_cadences_path(group)
click_link 'Done'
click_button cadence.title
expect(page).not_to have_content(iteration_in_cadence.title)
expect(page).to have_content(closed_iteration_in_cadence.title)
end
it 'shows inherited cadences in subgroup', :aggregate_failures do
visit group_iteration_cadences_path(subgroup)
expect(page).to have_content(cadence.title)
expect(page).to have_content(other_cadence.title)
expect(page).to have_content(subgroup_cadence.title)
end
end
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import * as JiraIssuesShowApi from 'ee/integrations/jira/issues_show/api';
import JiraIssuesShow from 'ee/integrations/jira/issues_show/components/jira_issues_show_root.vue';
import JiraIssueSidebar from 'ee/integrations/jira/issues_show/components/sidebar/jira_issues_sidebar_root.vue';
import { issueStates } from 'ee/integrations/jira/issues_show/constants';
import waitForPromises from 'helpers/wait_for_promises';
import IssuableHeader from '~/issuable_show/components/issuable_header.vue';
import IssuableShow from '~/issuable_show/components/issuable_show_root.vue';
import IssuableSidebar from '~/issuable_sidebar/components/issuable_sidebar_root.vue';
import axios from '~/lib/utils/axios_utils';
import { mockJiraIssue } from '../mock_data';
......@@ -18,14 +22,16 @@ describe('JiraIssuesShow', () => {
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findIssuableShow = () => wrapper.findComponent(IssuableShow);
const findJiraIssueSidebar = () => wrapper.findComponent(JiraIssueSidebar);
const findIssuableShowStatusBadge = () =>
wrapper.findComponent(IssuableHeader).find('[data-testid="status"]');
const createComponent = () => {
wrapper = shallowMount(JiraIssuesShow, {
stubs: {
IssuableShow,
IssuableHeader,
IssuableShow,
IssuableSidebar,
},
provide: {
issuesShowPath: mockJiraIssuesShowPath,
......@@ -39,11 +45,7 @@ describe('JiraIssuesShow', () => {
afterEach(() => {
mockAxios.restore();
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
wrapper.destroy();
});
describe('when issue is loading', () => {
......@@ -109,4 +111,45 @@ describe('JiraIssuesShow', () => {
expect(findIssuableShowStatusBadge().text()).toBe(badgeText);
});
});
describe('JiraIssueSidebar events', () => {
beforeEach(async () => {
mockAxios.onGet(mockJiraIssuesShowPath).replyOnce(200, mockJiraIssue);
createComponent();
await waitForPromises();
});
it('fetches issue statuses on issue-status-fetch', async () => {
const fetchIssueStatusesSpy = jest
.spyOn(JiraIssuesShowApi, 'fetchIssueStatuses')
.mockResolvedValue();
findJiraIssueSidebar().vm.$emit('issue-status-fetch');
await wrapper.vm.$nextTick();
expect(fetchIssueStatusesSpy).toHaveBeenCalled();
expect(findJiraIssueSidebar().props('isLoadingStatus')).toBe(true);
await waitForPromises();
expect(findJiraIssueSidebar().props('isLoadingStatus')).toBe(false);
});
it('updates issue status on issue-status-updated', async () => {
const updateIssueSpy = jest.spyOn(JiraIssuesShowApi, 'updateIssue').mockResolvedValue();
const status = 'In Review';
findJiraIssueSidebar().vm.$emit('issue-status-updated', status);
await wrapper.vm.$nextTick();
expect(updateIssueSpy).toHaveBeenCalledWith(expect.any(Object), { status });
expect(findJiraIssueSidebar().props('isUpdatingStatus')).toBe(true);
await waitForPromises();
expect(findJiraIssueSidebar().props('isUpdatingStatus')).toBe(false);
});
});
});
import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import IssueFieldDropdown from 'ee/integrations/jira/issues_show/components/sidebar/issue_field_dropdown.vue';
import { mockJiraIssueStatuses } from '../../mock_data';
describe('IssueFieldDropdown', () => {
let wrapper;
const emptyText = 'empty text';
const defaultProps = {
emptyText,
text: 'issue field text',
title: 'issue field header text',
};
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(IssueFieldDropdown, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
const findAllGlDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
it.each`
loading | items
${true} | ${[]}
${true} | ${mockJiraIssueStatuses}
${false} | ${[]}
${false} | ${mockJiraIssueStatuses}
`('with loading = $loading, items = $items', ({ loading, items }) => {
createComponent({
props: {
loading,
items,
},
});
expect(findGlLoadingIcon().exists()).toBe(loading);
if (!loading) {
if (items.length) {
findAllGlDropdownItems().wrappers.forEach((itemWrapper, index) => {
expect(itemWrapper.text()).toBe(mockJiraIssueStatuses[index].title);
});
} else {
expect(wrapper.text()).toBe(emptyText);
}
}
});
});
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlIcon } from '@gitlab/ui';
import IssueField from 'ee/integrations/jira/issues_show/components/sidebar/issue_field.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
describe('IssueField', () => {
let wrapper;
......@@ -15,37 +16,44 @@ describe('IssueField', () => {
};
const createComponent = ({ props = {} } = {}) => {
wrapper = extendedWrapper(
shallowMount(IssueField, {
propsData: { ...defaultProps, ...props },
directives: {
GlTooltip: createMockDirective(),
},
}),
);
wrapper = shallowMountExtended(IssueField, {
directives: {
GlTooltip: createMockDirective(),
},
propsData: { ...defaultProps, ...props },
stubs: {
SidebarEditableItem,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findFieldTitle = () => wrapper.findByTestId('field-title');
const findFieldValue = () => wrapper.findByTestId('field-value');
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findEditButton = () => wrapper.findComponent(GlButton);
const findFieldCollapsed = () => wrapper.findByTestId('field-collapsed');
const findFieldCollapsedTooltip = () => getBinding(findFieldCollapsed().element, 'gl-tooltip');
const findFieldValue = () => wrapper.findByTestId('field-value');
const findGlIcon = () => wrapper.findComponent(GlIcon);
it('renders title', () => {
createComponent();
describe('template', () => {
beforeEach(() => {
createComponent();
});
expect(findFieldTitle().text()).toBe(defaultProps.title);
});
it('renders title', () => {
expect(findEditableItem().props('title')).toBe(defaultProps.title);
});
it('renders GlIcon (when collapsed)', () => {
createComponent();
it('renders GlIcon (when collapsed)', () => {
expect(findGlIcon().props('name')).toBe(defaultProps.icon);
});
expect(findGlIcon().props('name')).toBe(defaultProps.icon);
it('does not render "Edit" button', () => {
expect(findEditButton().exists()).toBe(false);
});
});
describe('without value prop', () => {
......@@ -53,7 +61,7 @@ describe('IssueField', () => {
createComponent();
});
it('renders fallback value with "no-value" class', () => {
it('falls back to "None"', () => {
expect(findFieldValue().text()).toBe('None');
});
......@@ -74,7 +82,7 @@ describe('IssueField', () => {
});
});
it('renders value', () => {
it('renders the value', () => {
expect(findFieldValue().text()).toBe(value);
});
......@@ -85,4 +93,25 @@ describe('IssueField', () => {
expect(tooltip.value.title).toBe(value);
});
});
describe('with canUpdate = true', () => {
beforeEach(() => {
createComponent({
props: { canUpdate: true },
});
});
it('renders "Edit" button', () => {
expect(findEditButton().text()).toBe('Edit');
});
it('emits "issue-field-fetch" when dropdown is opened', () => {
wrapper.vm.$refs.dropdown.showDropdown = jest.fn();
findEditableItem().vm.$emit('open');
expect(wrapper.vm.$refs.dropdown.showDropdown).toHaveBeenCalled();
expect(wrapper.emitted('issue-field-fetch')).toHaveLength(1);
});
});
});
......@@ -25,10 +25,7 @@ describe('JiraIssuesSidebar', () => {
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
wrapper.destroy();
});
const findLabelsSelect = () => wrapper.findComponent(LabelsSelect);
......
......@@ -41,3 +41,5 @@ export const mockJiraIssueComment = {
},
id: 10000,
};
export const mockJiraIssueStatuses = [{ title: 'In Progress' }, { title: 'Done' }];
......@@ -22,7 +22,6 @@ describe('ManageFeature component', () => {
provide: {
glFeatures: {
secDependencyScanningUiEnable: true,
secSecretDetectionUiEnable: true,
},
},
...options,
......@@ -80,7 +79,6 @@ describe('ManageFeature component', () => {
it.each`
type | featureFlag
${REPORT_TYPE_DEPENDENCY_SCANNING} | ${'secDependencyScanningUiEnable'}
${REPORT_TYPE_SECRET_DETECTION} | ${'secSecretDetectionUiEnable'}
`('renders generic component for $type if $featureFlag is disabled', ({ type, featureFlag }) => {
const [feature] = generateFeatures(1, { type });
createComponent({
......
......@@ -167,8 +167,7 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
it 'detects security jobs even when the job has more than one report' do
config = { artifacts: { reports: { other_job: ['gl-other-report.json'], sast: ['gl-sast-report.json'] } } }
complicated_metadata = double(:complicated_metadata, config_options: config)
complicated_job = double(:complicated_job, metadata: complicated_metadata)
complicated_job = build_stubbed(:ci_build, options: config)
allow_next_instance_of(::Security::SecurityJobsFinder) do |finder|
allow(finder).to receive(:execute).and_return([complicated_job])
......
......@@ -47,7 +47,7 @@ RSpec.describe Ci::CreatePipelineService do
end
it 'persists cross_dependencies' do
deps = build_job.options['cross_dependencies']
deps = build_job.options[:cross_dependencies]
result = [
{ job: "job-1", ref: "ref-1", project: "project-1", artifacts: true },
{ job: "job-2", ref: "ref-2", project: "project-2", artifacts: false },
......@@ -142,7 +142,7 @@ RSpec.describe Ci::CreatePipelineService do
end
it 'persists cross_dependencies' do
deps = test_job.options['cross_dependencies']
deps = test_job.options[:cross_dependencies]
result = {
job: 'dependency',
ref: 'master',
......
......@@ -96,7 +96,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
expect(bridge).to be_a Ci::Bridge
expect(bridge.stage).to eq 'deploy'
expect(pipeline.statuses).to match_array [test, bridge]
expect(bridge.options).to eq('trigger' => { 'project' => 'my/project' })
expect(bridge.options).to eq(trigger: { project: 'my/project' })
expect(bridge.yaml_variables)
.to include(key: 'CROSS', value: 'downstream', public: true)
end
......
......@@ -83,15 +83,15 @@ RSpec.describe Ci::RunDastScanService do
it 'creates a build with appropriate options' do
build = pipeline.builds.first
expected_options = {
'image' => {
'name' => '$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION'
image: {
name: '$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION'
},
'script' => [
script: [
'/analyze'
],
'artifacts' => {
'reports' => {
'dast' => ['gl-dast-report.json']
artifacts: {
reports: {
dast: ['gl-dast-report.json']
}
}
}
......@@ -103,61 +103,61 @@ RSpec.describe Ci::RunDastScanService do
expected_variables = [
{
'key' => 'DAST_AUTH_URL',
'value' => dast_site_profile.auth_url,
'public' => true
key: 'DAST_AUTH_URL',
value: dast_site_profile.auth_url,
public: true
}, {
'key' => 'DAST_DEBUG',
'value' => String(dast_scanner_profile.show_debug_messages?),
'public' => true
key: 'DAST_DEBUG',
value: String(dast_scanner_profile.show_debug_messages?),
public: true
}, {
'key' => 'DAST_EXCLUDE_URLS',
'value' => dast_site_profile.excluded_urls.join(','),
'public' => true
key: 'DAST_EXCLUDE_URLS',
value: dast_site_profile.excluded_urls.join(','),
public: true
}, {
'key' => 'DAST_FULL_SCAN_ENABLED',
'value' => String(dast_scanner_profile.full_scan_enabled?),
'public' => true
key: 'DAST_FULL_SCAN_ENABLED',
value: String(dast_scanner_profile.full_scan_enabled?),
public: true
}, {
'key' => 'DAST_PASSWORD_FIELD',
'value' => dast_site_profile.auth_password_field,
'public' => true
key: 'DAST_PASSWORD_FIELD',
value: dast_site_profile.auth_password_field,
public: true
}, {
'key' => 'DAST_SPIDER_MINS',
'value' => String(dast_scanner_profile.spider_timeout),
'public' => true
key: 'DAST_SPIDER_MINS',
value: String(dast_scanner_profile.spider_timeout),
public: true
}, {
'key' => 'DAST_TARGET_AVAILABILITY_TIMEOUT',
'value' => String(dast_scanner_profile.target_timeout),
'public' => true
key: 'DAST_TARGET_AVAILABILITY_TIMEOUT',
value: String(dast_scanner_profile.target_timeout),
public: true
}, {
'key' => 'DAST_USERNAME',
'value' => dast_site_profile.auth_username,
'public' => true
key: 'DAST_USERNAME',
value: dast_site_profile.auth_username,
public: true
}, {
'key' => 'DAST_USERNAME_FIELD',
'value' => dast_site_profile.auth_username_field,
'public' => true
key: 'DAST_USERNAME_FIELD',
value: dast_site_profile.auth_username_field,
public: true
}, {
'key' => 'DAST_USE_AJAX_SPIDER',
'value' => String(dast_scanner_profile.use_ajax_spider?),
'public' => true
key: 'DAST_USE_AJAX_SPIDER',
value: String(dast_scanner_profile.use_ajax_spider?),
public: true
}, {
'key' => 'DAST_VERSION',
'value' => '1',
'public' => true
key: 'DAST_VERSION',
value: '1',
public: true
}, {
'key' => 'DAST_WEBSITE',
'value' => dast_site_profile.dast_site.url,
'public' => true
key: 'DAST_WEBSITE',
value: dast_site_profile.dast_site.url,
public: true
}, {
'key' => 'GIT_STRATEGY',
'value' => 'none',
'public' => true
key: 'GIT_STRATEGY',
value: 'none',
public: true
}, {
'key' => 'SECURE_ANALYZERS_PREFIX',
'value' => 'registry.gitlab.com/gitlab-org/security-products/analyzers',
'public' => true
key: 'SECURE_ANALYZERS_PREFIX',
value: 'registry.gitlab.com/gitlab-org/security-products/analyzers',
public: true
}
]
......
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Background migration for deleting orphaned deployments.
class DeleteOrphanedDeployments
include Database::MigrationHelpers
def perform(start_id, end_id)
orphaned_deployments
.where(id: start_id..end_id)
.delete_all
mark_job_as_succeeded(start_id, end_id)
end
def orphaned_deployments
define_batchable_model('deployments')
.where('NOT EXISTS (SELECT 1 FROM environments WHERE deployments.environment_id = environments.id)')
end
private
def mark_job_as_succeeded(*arguments)
Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
self.class.name.demodulize,
arguments
)
end
end
end
end
......@@ -189,7 +189,7 @@ module Gitlab
raise ArchiveError, 'Job is not finished yet' unless job.complete?
if trace_artifact
unsafe_trace_cleanup! if Feature.enabled?(:erase_traces_from_already_archived_jobs_when_archiving_again, job.project, default_enabled: :yaml)
unsafe_trace_cleanup!
raise AlreadyArchivedError, 'Could not archive again'
end
......
......@@ -27,21 +27,18 @@ module Gitlab
# connect with Spamcheck
@endpoint_url = @endpoint_url.gsub(%r(^grpc:\/\/), '')
creds =
@creds =
if Rails.env.development? || Rails.env.test?
:this_channel_is_insecure
else
GRPC::Core::ChannelCredentials.new
end
@stub = ::Spamcheck::SpamcheckService::Stub.new(@endpoint_url, creds,
timeout: DEFAULT_TIMEOUT_SECS)
end
def issue_spam?(spam_issue:, user:, context: {})
issue = build_issue_protobuf(issue: spam_issue, user: user, context: context)
response = @stub.check_for_spam_issue(issue,
response = grpc_client.check_for_spam_issue(issue,
metadata: { 'authorization' =>
Gitlab::CurrentSettings.spam_check_api_key })
verdict = convert_verdict_to_gitlab_constant(response.verdict)
......@@ -100,6 +97,16 @@ module Gitlab
Google::Protobuf::Timestamp.new(seconds: ar_timestamp.to_time.to_i,
nanos: ar_timestamp.to_time.nsec)
end
def grpc_client
@grpc_client ||= ::Spamcheck::SpamcheckService::Stub.new(@endpoint_url, @creds,
interceptors: interceptors,
timeout: DEFAULT_TIMEOUT_SECS)
end
def interceptors
[Labkit::Correlation::GRPC::ClientInterceptor.instance]
end
end
end
end
......@@ -169,6 +169,16 @@ module Gitlab
end
end
def deep_symbolized_access(data)
if data.is_a?(Array)
data.map(&method(:deep_symbolized_access))
elsif data.is_a?(Hash)
data.deep_symbolize_keys
else
data
end
end
def string_to_ip_object(str)
return unless str
......
# frozen_string_literal: true
module Serializers
# Make the resulting hash have deep symbolized keys
class SymbolizedJson
class << self
def dump(obj)
obj
end
def load(data)
return if data.nil?
Gitlab::Utils.deep_symbolized_access(data)
end
end
end
end
......@@ -18406,9 +18406,15 @@ msgstr ""
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
msgid "JiraService|Failed to load Jira issue statuses. View the issue in Jira, or reload the page."
msgstr ""
msgid "JiraService|Failed to load Jira issue. View the issue in Jira, or reload the page."
msgstr ""
msgid "JiraService|Failed to update Jira issue status. View the issue in Jira, or reload the page."
msgstr ""
msgid "JiraService|Fetch issue types for this Jira project"
msgstr ""
......@@ -18457,6 +18463,9 @@ msgstr ""
msgid "JiraService|Move to Done"
msgstr ""
msgid "JiraService|No available statuses"
msgstr ""
msgid "JiraService|Not all data may be displayed here. To view more details or make changes to this issue, go to %{linkStart}Jira%{linkEnd}."
msgstr ""
......
......@@ -60,6 +60,7 @@ module DeprecationToolkitEnv
activerecord-6.0.3.7/lib/active_record/relation.rb
asciidoctor-2.0.12/lib/asciidoctor/extensions.rb
attr_encrypted-3.1.0/lib/attr_encrypted/adapters/active_record.rb
gitlab-labkit-0.18.0/lib/labkit/correlation/grpc/client_interceptor.rb
]
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedDeployments, :migration, schema: 20210617161348 do
let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
let!(:environment) { table(:environments).create!(name: 'production', slug: 'production', project_id: project.id) }
let(:background_migration_jobs) { table(:background_migration_jobs) }
before do
create_deployment!(environment.id, project.id)
create_deployment!(non_existing_record_id, project.id)
end
it 'deletes only orphaned deployments' do
expect(valid_deployments.pluck(:id)).not_to be_empty
expect(orphaned_deployments.pluck(:id)).not_to be_empty
subject.perform(table(:deployments).minimum(:id), table(:deployments).maximum(:id))
expect(valid_deployments.pluck(:id)).not_to be_empty
expect(orphaned_deployments.pluck(:id)).to be_empty
end
it 'marks jobs as done' do
first_job = background_migration_jobs.create!(
class_name: 'DeleteOrphanedDeployments',
arguments: [table(:deployments).minimum(:id), table(:deployments).minimum(:id)]
)
second_job = background_migration_jobs.create!(
class_name: 'DeleteOrphanedDeployments',
arguments: [table(:deployments).maximum(:id), table(:deployments).maximum(:id)]
)
subject.perform(table(:deployments).minimum(:id), table(:deployments).minimum(:id))
expect(first_job.reload.status).to eq(Gitlab::Database::BackgroundMigrationJob.statuses[:succeeded])
expect(second_job.reload.status).to eq(Gitlab::Database::BackgroundMigrationJob.statuses[:pending])
end
private
def valid_deployments
table(:deployments).where('EXISTS (SELECT 1 FROM environments WHERE deployments.environment_id = environments.id)')
end
def orphaned_deployments
table(:deployments).where('NOT EXISTS (SELECT 1 FROM environments WHERE deployments.environment_id = environments.id)')
end
def create_deployment!(environment_id, project_id)
table(:deployments).create!(
environment_id: environment_id,
project_id: project_id,
ref: 'master',
tag: false,
sha: 'x',
status: 1,
iid: table(:deployments).count + 1)
end
end
......@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
let(:endpoint) { 'grpc://grpc.test.url' }
let_it_be(:user) { create(:user, organization: 'GitLab') }
let(:verdict_value) { nil }
let(:verdict_value) { ::Spamcheck::SpamVerdict::Verdict::ALLOW }
let(:error_value) { "" }
let(:attribs_value) do
......@@ -56,6 +56,13 @@ RSpec.describe Gitlab::Spamcheck::Client do
expect(subject).to eq([expected, { "monitorMode" => "false" }, ""])
end
end
it 'includes interceptors' do
expect_next_instance_of(::Gitlab::Spamcheck::Client) do |client|
expect(client).to receive(:interceptors).and_call_original
end
subject
end
end
describe "#build_issue_protobuf", :aggregate_failures do
......
......@@ -351,6 +351,22 @@ RSpec.describe Gitlab::Utils do
end
end
describe '.deep_symbolized_access' do
let(:hash) do
{ "variables" => [{ "key" => "VAR1", "value" => "VALUE2" }] }
end
subject { described_class.deep_symbolized_access(hash) }
it 'allows to access hash keys with symbols' do
expect(subject[:variables]).to be_a(Array)
end
it 'allows to access array keys with symbols' do
expect(subject[:variables].first[:key]).to eq('VAR1')
end
end
describe '.try_megabytes_to_bytes' do
context 'when the size can be converted to megabytes' do
it 'returns the size in megabytes' do
......
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Serializers::SymbolizedJson do
describe '.dump' do
let(:obj) { { key: "value" } }
subject { described_class.dump(obj) }
it 'returns a hash' do
is_expected.to eq(obj)
end
end
describe '.load' do
let(:data_string) { '{"key":"value","variables":[{"key":"VAR1","value":"VALUE1"}]}' }
let(:data_hash) { Gitlab::Json.parse(data_string) }
context 'when loading a hash' do
subject { described_class.load(data_hash) }
it 'decodes a string' do
is_expected.to be_a(Hash)
end
it 'allows to access with symbols' do
expect(subject[:key]).to eq('value')
expect(subject[:variables].first[:key]).to eq('VAR1')
end
end
context 'when loading a nil' do
subject { described_class.load(nil) }
it 'returns nil' do
is_expected.to be_nil
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe ScheduleDeleteOrphanedDeployments, :sidekiq, schema: 20210617161348 do
let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
let!(:environment) { table(:environments).create!(name: 'production', slug: 'production', project_id: project.id) }
let(:background_migration_jobs) { table(:background_migration_jobs) }
before do
create_deployment!(environment.id, project.id)
create_deployment!(environment.id, project.id)
create_deployment!(environment.id, project.id)
create_deployment!(non_existing_record_id, project.id)
create_deployment!(non_existing_record_id, project.id)
create_deployment!(non_existing_record_id, project.id)
create_deployment!(non_existing_record_id, project.id)
stub_const("#{described_class}::BATCH_SIZE", 1)
end
it 'schedules DeleteOrphanedDeployments background jobs' do
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(7)
table(:deployments).find_each do |deployment|
expect(described_class::MIGRATION).to be_scheduled_migration(deployment.id, deployment.id)
end
end
end
end
def create_deployment!(environment_id, project_id)
table(:deployments).create!(
environment_id: environment_id,
project_id: project_id,
ref: 'master',
tag: false,
sha: 'x',
status: 1,
iid: table(:deployments).count + 1)
end
end
......@@ -2172,15 +2172,15 @@ RSpec.describe Ci::Build do
end
it 'contains options' do
expect(build.options).to eq(options.stringify_keys)
expect(build.options).to eq(options.symbolize_keys)
end
it 'allows to access with keys' do
it 'allows to access with symbolized keys' do
expect(build.options[:image]).to eq('ruby:2.7')
end
it 'allows to access with strings' do
expect(build.options['image']).to eq('ruby:2.7')
it 'rejects access with string keys' do
expect(build.options['image']).to be_nil
end
context 'when ci_build_metadata_config is set' do
......@@ -2189,7 +2189,7 @@ RSpec.describe Ci::Build do
end
it 'persist data in build metadata' do
expect(build.metadata.read_attribute(:config_options)).to eq(options.stringify_keys)
expect(build.metadata.read_attribute(:config_options)).to eq(options.symbolize_keys)
end
it 'does not persist data in build' do
......@@ -4715,9 +4715,9 @@ RSpec.describe Ci::Build do
describe '#read_metadata_attribute' do
let(:build) { create(:ci_build, :degenerated) }
let(:build_options) { { "key" => "build" } }
let(:metadata_options) { { "key" => "metadata" } }
let(:default_options) { { "key" => "default" } }
let(:build_options) { { key: "build" } }
let(:metadata_options) { { key: "metadata" } }
let(:default_options) { { key: "default" } }
subject { build.send(:read_metadata_attribute, :options, :config_options, default_options) }
......@@ -4752,8 +4752,8 @@ RSpec.describe Ci::Build do
describe '#write_metadata_attribute' do
let(:build) { create(:ci_build, :degenerated) }
let(:options) { { "key" => "new options" } }
let(:existing_options) { { "key" => "existing options" } }
let(:options) { { key: "new options" } }
let(:existing_options) { { key: "existing options" } }
subject { build.send(:write_metadata_attribute, :options, :config_options, options) }
......
......@@ -30,43 +30,17 @@ RSpec.describe Ci::ArchiveTraceService, '#execute' do
create(:ci_build_trace_chunk, build: job)
end
context 'when the feature flag `erase_traces_from_already_archived_jobs_when_archiving_again` is enabled' do
before do
stub_feature_flags(erase_traces_from_already_archived_jobs_when_archiving_again: true)
end
it 'removes the trace chunks' do
expect { subject }.to change { job.trace_chunks.count }.to(0)
end
context 'when associated data does not exist' do
before do
job.job_artifacts_trace.file.remove!
end
it 'removes the trace artifact' do
expect { subject }.to change { job.reload.job_artifacts_trace }.to(nil)
end
end
it 'removes the trace chunks' do
expect { subject }.to change { job.trace_chunks.count }.to(0)
end
context 'when the feature flag `erase_traces_from_already_archived_jobs_when_archiving_again` is disabled' do
context 'when associated data does not exist' do
before do
stub_feature_flags(erase_traces_from_already_archived_jobs_when_archiving_again: false)
job.job_artifacts_trace.file.remove!
end
it 'does not remove the trace chunks' do
expect { subject }.not_to change { job.trace_chunks.count }
end
context 'when associated data does not exist' do
before do
job.job_artifacts_trace.file.remove!
end
it 'does not remove the trace artifact' do
expect { subject }.not_to change { job.reload.job_artifacts_trace }
end
it 'removes the trace artifact' do
expect { subject }.to change { job.reload.job_artifacts_trace }.to(nil)
end
end
end
......
......@@ -33,11 +33,11 @@ RSpec.describe Ci::CreatePipelineService do
it 'uses the provided key' do
expected = {
'key' => 'a-key',
'paths' => ['logs/', 'binaries/'],
'policy' => 'pull-push',
'untracked' => true,
'when' => 'on_success'
key: 'a-key',
paths: ['logs/', 'binaries/'],
policy: 'pull-push',
untracked: true,
when: 'on_success'
}
expect(pipeline).to be_persisted
......@@ -66,10 +66,10 @@ RSpec.describe Ci::CreatePipelineService do
it 'builds a cache key' do
expected = {
'key' => /[a-f0-9]{40}/,
'paths' => ['logs/'],
'policy' => 'pull-push',
'when' => 'on_success'
key: /[a-f0-9]{40}/,
paths: ['logs/'],
policy: 'pull-push',
when: 'on_success'
}
expect(pipeline).to be_persisted
......@@ -82,10 +82,10 @@ RSpec.describe Ci::CreatePipelineService do
it 'uses default cache key' do
expected = {
'key' => /default/,
'paths' => ['logs/'],
'policy' => 'pull-push',
'when' => 'on_success'
key: /default/,
paths: ['logs/'],
policy: 'pull-push',
when: 'on_success'
}
expect(pipeline).to be_persisted
......@@ -115,10 +115,10 @@ RSpec.describe Ci::CreatePipelineService do
it 'builds a cache key' do
expected = {
'key' => /\$ENV_VAR-[a-f0-9]{40}/,
'paths' => ['logs/'],
'policy' => 'pull-push',
'when' => 'on_success'
key: /\$ENV_VAR-[a-f0-9]{40}/,
paths: ['logs/'],
policy: 'pull-push',
when: 'on_success'
}
expect(pipeline).to be_persisted
......@@ -131,10 +131,10 @@ RSpec.describe Ci::CreatePipelineService do
it 'uses default cache key' do
expected = {
'key' => /\$ENV_VAR-default/,
'paths' => ['logs/'],
'policy' => 'pull-push',
'when' => 'on_success'
key: /\$ENV_VAR-default/,
paths: ['logs/'],
policy: 'pull-push',
when: 'on_success'
}
expect(pipeline).to be_persisted
......
......@@ -39,8 +39,8 @@ RSpec.describe Ci::CreatePipelineService do
it 'creates a pipeline' do
expect(pipeline).to be_persisted
expect(pipeline.builds.first.options).to match(a_hash_including({
'before_script' => ['ls'],
'script' => [
before_script: ['ls'],
script: [
'echo doing my first step',
'echo doing step 1 of job 1',
'echo doing my last step'
......
......@@ -104,7 +104,7 @@ RSpec.describe Ci::CreatePipelineService do
it 'saves dependencies' do
expect(test_a_build.options)
.to match(a_hash_including('dependencies' => ['build_a']))
.to match(a_hash_including(dependencies: ['build_a']))
end
it 'artifacts default to true' do
......
......@@ -69,9 +69,9 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do
let(:expected_bridge_options) do
{
'trigger' => {
'include' => [
{ 'local' => 'path/to/child.yml' }
trigger: {
include: [
{ local: 'path/to/child.yml' }
]
}
}
......@@ -149,9 +149,9 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do
let(:expected_bridge_options) do
{
'trigger' => {
'include' => [
{ 'local' => 'path/to/child.yml' }
trigger: {
include: [
{ local: 'path/to/child.yml' }
]
}
}
......@@ -175,8 +175,8 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do
let(:expected_bridge_options) do
{
'trigger' => {
'include' => 'path/to/child.yml'
trigger: {
include: 'path/to/child.yml'
}
}
end
......@@ -202,8 +202,8 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do
let(:expected_bridge_options) do
{
'trigger' => {
'include' => ['path/to/child.yml', 'path/to/child2.yml']
trigger: {
include: ['path/to/child.yml', 'path/to/child2.yml']
}
}
end
......@@ -295,12 +295,12 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do
let(:expected_bridge_options) do
{
'trigger' => {
'include' => [
trigger: {
include: [
{
'file' => 'path/to/child.yml',
'project' => 'my-namespace/my-project',
'ref' => 'master'
file: 'path/to/child.yml',
project: 'my-namespace/my-project',
ref: 'master'
}
]
}
......@@ -353,11 +353,11 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do
it_behaves_like 'successful creation' do
let(:expected_bridge_options) do
{
'trigger' => {
'include' => [
trigger: {
include: [
{
'file' => ["path/to/child1.yml", "path/to/child2.yml"],
'project' => 'my-namespace/my-project'
file: ["path/to/child1.yml", "path/to/child2.yml"],
project: 'my-namespace/my-project'
}
]
}
......
......@@ -1001,7 +1001,7 @@ RSpec.describe Ci::CreatePipelineService do
expect(pipeline.yaml_errors).not_to be_present
expect(pipeline).to be_persisted
expect(build).to be_kind_of(Ci::Build)
expect(build.options).to eq(config[:release].except(:stage, :only).with_indifferent_access)
expect(build.options).to eq(config[:release].except(:stage, :only))
expect(build).to be_persisted
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