Commit 7f5f9400 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@13-5-stable-ee

parent b16db145
......@@ -2,6 +2,16 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 13.5.4 (2020-11-13)
### Fixed (4 changes)
- Fix Vue Labels Select dropdown keyboard scroll. !43874
- Hashed Storage: make migration and rollback resilient to exceptions. !46178
- Fix compliance framework database migration on CE instances. !46761
- Resolve problem when namespace_settings were not created for groups created via admin panel. !46875
## 13.5.3 (2020-11-03)
### Fixed (3 changes)
......
13.5.3
\ No newline at end of file
13.5.4
\ No newline at end of file
......@@ -3,5 +3,3 @@ export const DropdownVariant = {
Standalone: 'standalone',
Embedded: 'embedded',
};
export const LIST_BUFFER_SIZE = 5;
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import {
GlIntersectionObserver,
GlLoadingIcon,
GlButton,
GlSearchBoxByType,
GlLink,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import LabelItem from './label_item.vue';
import { LIST_BUFFER_SIZE } from './constants';
export default {
LIST_BUFFER_SIZE,
components: {
GlIntersectionObserver,
GlLoadingIcon,
GlButton,
GlSearchBoxByType,
GlLink,
SmartVirtualList,
LabelItem,
},
data() {
......@@ -46,15 +48,8 @@ export default {
}
return this.labels;
},
showListContainer() {
if (this.isDropdownVariantSidebar) {
return !this.labelsFetchInProgress;
}
return true;
},
showNoMatchingResultsMessage() {
return !this.labelsFetchInProgress && !this.visibleLabels.length;
return Boolean(this.searchKey) && this.visibleLabels.length === 0;
},
},
watch: {
......@@ -67,14 +62,12 @@ export default {
}
},
},
mounted() {
this.fetchLabels();
},
methods: {
...mapActions([
'toggleDropdownContents',
'toggleDropdownContentsCreateView',
'fetchLabels',
'receiveLabelsSuccess',
'updateSelectedLabels',
'toggleDropdownContents',
]),
......@@ -99,6 +92,17 @@ export default {
}
}
},
/**
* We want to remove loaded labels to ensure component
* fetches fresh set of labels every time when shown.
*/
handleComponentDisappear() {
this.receiveLabelsSuccess([]);
},
handleCreateLabelClick() {
this.receiveLabelsSuccess([]);
this.toggleDropdownContentsCreateView();
},
/**
* This method enables keyboard navigation support for
* the dropdown.
......@@ -135,84 +139,75 @@ export default {
</script>
<template>
<div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
<gl-loading-icon
v-if="labelsFetchInProgress"
class="labels-fetch-loading position-absolute gl-display-flex gl-align-items-center w-100 h-100"
size="md"
/>
<div
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
data-testid="dropdown-title"
>
<span class="flex-grow-1">{{ labelsListTitle }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
@click="toggleDropdownContents"
/>
</div>
<div class="dropdown-input" @click.stop="() => {}">
<gl-search-box-by-type
v-model="searchKey"
:autofocus="true"
data-qa-selector="dropdown_input_field"
/>
</div>
<div
v-show="showListContainer"
ref="labelsListContainer"
class="dropdown-content"
data-testid="dropdown-content"
>
<smart-virtual-list
:length="visibleLabels.length"
:remain="$options.LIST_BUFFER_SIZE"
:size="$options.LIST_BUFFER_SIZE"
wclass="list-unstyled mb-0"
wtag="ul"
class="h-100"
<gl-intersection-observer @appear="fetchLabels" @disappear="handleComponentDisappear">
<div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
<div
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
data-testid="dropdown-title"
>
<li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
<span class="flex-grow-1">{{ labelsListTitle }}</span>
<gl-button
:aria-label="__('Close')"
variant="link"
size="small"
class="dropdown-header-button gl-p-0!"
icon="close"
@click="toggleDropdownContents"
/>
</div>
<div class="dropdown-input" @click.stop="() => {}">
<gl-search-box-by-type
v-model="searchKey"
:autofocus="true"
:disabled="labelsFetchInProgress"
data-qa-selector="dropdown_input_field"
/>
</div>
<div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
<gl-loading-icon
v-if="labelsFetchInProgress"
class="labels-fetch-loading gl-align-items-center w-100 h-100"
size="md"
/>
<ul v-else class="list-unstyled mb-0">
<label-item
v-for="(label, index) in visibleLabels"
:key="label.id"
:label="label"
:is-label-set="label.set"
:highlight="index === currentHighlightItem"
@clickLabel="handleLabelClick(label)"
/>
</li>
<li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center">
{{ __('No matching results') }}
</li>
</smart-virtual-list>
</div>
<div
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
class="dropdown-footer"
data-testid="dropdown-footer"
>
<ul class="list-unstyled">
<li v-if="allowLabelCreate">
<gl-link
class="gl-display-flex w-100 flex-row text-break-word label-item"
@click="toggleDropdownContentsCreateView"
>
{{ footerCreateLabelTitle }}
</gl-link>
</li>
<li>
<gl-link
:href="labelsManagePath"
class="gl-display-flex flex-row text-break-word label-item"
>
{{ footerManageLabelTitle }}
</gl-link>
</li>
</ul>
<li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center">
{{ __('No matching results') }}
</li>
</ul>
</div>
<div
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
class="dropdown-footer"
data-testid="dropdown-footer"
>
<ul class="list-unstyled">
<li v-if="allowLabelCreate">
<gl-link
class="gl-display-flex w-100 flex-row text-break-word label-item"
@click="handleCreateLabelClick"
>
{{ footerCreateLabelTitle }}
</gl-link>
</li>
<li>
<gl-link
:href="labelsManagePath"
class="gl-display-flex flex-row text-break-word label-item"
>
{{ footerManageLabelTitle }}
</gl-link>
</li>
</ul>
</div>
</div>
</div>
</gl-intersection-observer>
</template>
<script>
import { GlIcon, GlLink } from '@gitlab/ui';
import { GlLink, GlIcon } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlLink,
},
functional: true,
props: {
label: {
type: Object,
......@@ -21,46 +18,65 @@ export default {
default: false,
},
},
data() {
return {
isSet: this.isLabelSet,
};
},
computed: {
labelBoxStyle() {
return {
backgroundColor: this.label.color,
};
},
},
watch: {
/**
* This watcher assures that if user used
* `Enter` key to set/unset label, changes
* are reflected here too.
*/
isLabelSet(value) {
this.isSet = value;
},
},
methods: {
handleClick() {
this.isSet = !this.isSet;
this.$emit('clickLabel', this.label);
},
render(h, { props, listeners }) {
const { label, highlight, isLabelSet } = props;
const labelColorBox = h('span', {
class: 'dropdown-label-box',
style: {
backgroundColor: label.color,
},
attrs: {
'data-testid': 'label-color-box',
},
});
const checkedIcon = h(GlIcon, {
class: {
'mr-2 align-self-center': true,
hidden: !isLabelSet,
},
props: {
name: 'mobile-issue-close',
},
});
const noIcon = h('span', {
class: {
'mr-3 pr-2': true,
hidden: isLabelSet,
},
attrs: {
'data-testid': 'no-icon',
},
});
const labelTitle = h('span', label.title);
const labelLink = h(
GlLink,
{
class: 'd-flex align-items-baseline text-break-word label-item',
on: {
click: () => {
listeners.clickLabel(label);
},
},
},
[noIcon, checkedIcon, labelColorBox, labelTitle],
);
return h(
'li',
{
class: {
'd-block': true,
'text-left': true,
'is-focused': highlight,
},
},
[labelLink],
);
},
};
</script>
<template>
<gl-link
class="d-flex align-items-baseline text-break-word label-item"
:class="{ 'is-focused': highlight }"
@click="handleClick"
>
<gl-icon v-show="isSet" name="mobile-issue-close" class="mr-2 align-self-center" />
<span v-show="!isSet" data-testid="no-icon" class="mr-3 pr-2"></span>
<span class="dropdown-label-box" data-testid="label-color-box" :style="labelBoxStyle"></span>
<span>{{ label.title }}</span>
</gl-link>
</template>
......@@ -266,7 +266,7 @@ export default {
</dropdown-value>
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
<dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents"
v-show="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
/>
</template>
......
......@@ -1016,6 +1016,23 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
}
}
li {
&:hover,
&.is-focused {
.label-item {
@include dropdown-item-hover;
text-decoration: none;
}
}
}
.labels-select-dropdown-button {
.gl-button-text {
width: 100%;
}
}
.labels-select-dropdown-contents {
min-height: $dropdown-min-height;
max-height: 330px;
......@@ -1049,13 +1066,6 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
.label-item {
padding: 8px 20px;
&:hover,
&.is-focused {
@include dropdown-item-hover;
text-decoration: none;
}
}
.color-input-container {
......
......@@ -41,6 +41,7 @@ class Admin::GroupsController < Admin::ApplicationController
if @group.save
@group.add_owner(current_user)
@group.create_namespace_settings
redirect_to [:admin, @group], notice: _('Group %{group_name} was successfully created.') % { group_name: @group.name }
else
render "new"
......
......@@ -21,14 +21,32 @@ module Projects
project.storage_version = nil
end
project.repository_read_only = false
project.save!(validate: false)
if result && block_given?
yield
project.transaction do
project.save!(validate: false)
project.set_repository_writable!
end
result
rescue Gitlab::Git::CommandError => e
logger.error("Repository #{project.full_path} failed to upgrade (PROJECT_ID=#{project.id}). Git operation failed: #{e.inspect}")
rollback_migration!
false
rescue OpenSSL::Cipher::CipherError => e
logger.error("Repository #{project.full_path} failed to upgrade (PROJECT_ID=#{project.id}). There is a problem with encrypted attributes: #{e.inspect}")
rollback_migration!
false
end
private
def rollback_migration!
rollback_folder_move
project.storage_version = nil
project.set_repository_writable!
end
end
end
......
......@@ -21,14 +21,32 @@ module Projects
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
end
project.repository_read_only = false
project.save!(validate: false)
if result && block_given?
yield
project.transaction do
project.save!(validate: false)
project.set_repository_writable!
end
result
rescue Gitlab::Git::CommandError => e
logger.error("Repository #{project.full_path} failed to rollback (PROJECT_ID=#{project.id}). Git operation failed: #{e.inspect}")
rollback_migration!
false
rescue OpenSSL::Cipher::CipherError => e
logger.error("Repository #{project.full_path} failed to rollback (PROJECT_ID=#{project.id}). There is a problem with encrypted attributes: #{e.inspect}")
rollback_migration!
false
end
private
def rollback_migration!
rollback_folder_move
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
project.set_repository_writable!
end
end
end
......
......@@ -52,8 +52,6 @@ class MigrateComplianceFrameworkEnumToDatabaseFrameworkRecord < ActiveRecord::Mi
end
def up
return unless Gitlab.ee?
TmpComplianceFramework.reset_column_information
TmpProjectSettings.reset_column_information
......
# frozen_string_literal: true
class EnsureNamespaceSettingsCreation < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 10000
MIGRATION = 'BackfillNamespaceSettings'
DELAY_INTERVAL = 2.minutes.to_i
disable_ddl_transaction!
class Namespace < ActiveRecord::Base
include EachBatch
self.table_name = 'namespaces'
end
def up
ensure_data_migration
end
def down
# no-op
end
private
def ensure_data_migration
Namespace.each_batch(of: BATCH_SIZE) do |query, index|
missing_count = query.where("NOT EXISTS (SELECT 1 FROM namespace_settings WHERE namespace_settings.namespace_id=namespaces.id)").limit(1).size
if missing_count > 0
ids_range = query.pluck("MIN(id), MAX(id)").flatten
migrate_in(index * DELAY_INTERVAL, MIGRATION, ids_range)
end
end
end
end
e17da7eebb6d054a711368369d2b4fa684e96344f845bb7c6b3c89a9b4c4e067
\ No newline at end of file
......@@ -5,51 +5,29 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference, concepts
---
# Instance-level merge request approval rules **(PREMIUM ONLY)**
# Merge request approval rules **(PREMIUM ONLY)**
> Introduced in [GitLab Premium](https://gitlab.com/gitlab-org/gitlab/-/issues/39060) 12.8.
Merge request approvals rules prevent users overriding certain settings on a project
level. When configured, only administrators can change these settings on a project level
if they are enabled at an instance level.
Merge request approval rules prevent users from overriding certain settings on the project
level. When enabled at the instance level, these settings are no longer editable on the
project level.
To enable merge request approval rules for an instance:
1. Navigate to **Admin Area >** **{push-rules}** **Push Rules** and expand **Merge
requests approvals**.
requests approvals**.
1. Set the required rule.
1. Click **Save changes**.
GitLab administrators can later override these settings in a project’s settings.
## Available rules
Merge request approval rules that can be set at an instance level are:
- **Prevent approval of merge requests by merge request author**. Prevents project
maintainers from allowing request authors to merge their own merge requests.
maintainers from allowing request authors to merge their own merge requests.
- **Prevent approval of merge requests by merge request committers**. Prevents project
maintainers from allowing users to approve merge requests if they have submitted
any commits to the source branch.
- **Prevent users from modifying merge request approvers list**. Prevents project
maintainers from allowing users to modify the approvers list in project settings
or in individual merge requests.
## Scope rules to compliance-labeled projects
> Introduced in [GitLab Premium](https://gitlab.com/groups/gitlab-org/-/epics/3432) 13.2.
Merge request approval rules can be further scoped to specific compliance frameworks.
When the compliance framework label is selected and the project is assigned the compliance
label, the instance-level MR approval settings will take effect and the
[project-level settings](../project/merge_requests/merge_request_approvals.md#adding--editing-a-default-approval-rule)
is locked for modification.
When the compliance framework label is not selected or the project is not assigned the
compliance label, the project-level MR approval settings will take effect and the users with
Maintainer role and above can modify these.
| Instance-level | Project-level |
| -------------- | ------------- |
| ![Scope MR approval settings to compliance frameworks](img/scope_mr_approval_settings_v13_1.png) | ![MR approval settings on compliance projects](img/mr_approval_settings_compliance_project_v13_1.png) |
maintainers from allowing users to approve merge requests if they have submitted
any commits to the source branch.
- **Prevent users from modifying merge request approvers list**. Prevents users from
modifying the approvers list in project settings or in individual merge requests.
......@@ -6817,9 +6817,6 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
msgid "Compliance frameworks"
msgstr ""
msgid "ComplianceDashboard|created by:"
msgstr ""
......@@ -21738,9 +21735,6 @@ msgstr ""
msgid "Registry setup"
msgstr ""
msgid "Regulate approvals by authors/committers, based on compliance frameworks. Can be changed only at the instance level."
msgstr ""
msgid "Reindexing status"
msgstr ""
......@@ -26064,9 +26058,6 @@ msgstr ""
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr ""
msgid "The above settings apply to all projects with the selected compliance framework(s)."
msgstr ""
msgid "The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential."
msgstr ""
......
......@@ -25,6 +25,20 @@ RSpec.describe Admin::GroupsController do
end
end
describe 'POST #create' do
it 'creates group' do
expect do
post :create, params: { group: { path: 'test', name: 'test' } }
end.to change { Group.count }.by(1)
end
it 'creates namespace_settings for group' do
expect do
post :create, params: { group: { path: 'test', name: 'test' } }
end.to change { NamespaceSetting.count }.by(1)
end
end
describe 'PUT #members_update' do
let(:group_user) { create(:user) }
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import {
GlIntersectionObserver,
GlButton,
GlLoadingIcon,
GlSearchBoxByType,
GlLink,
} from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
......@@ -88,20 +93,25 @@ describe('DropdownContentsLabelsView', () => {
});
});
describe('showListContainer', () => {
describe('showNoMatchingResultsMessage', () => {
it.each`
variant | loading | showList
${'sidebar'} | ${false} | ${true}
${'sidebar'} | ${true} | ${false}
${'not-sidebar'} | ${true} | ${true}
${'not-sidebar'} | ${false} | ${true}
searchKey | labels | labelsDescription | returnValue
${''} | ${[]} | ${'empty'} | ${false}
${'bug'} | ${[]} | ${'empty'} | ${true}
${''} | ${mockLabels} | ${'not empty'} | ${false}
${'bug'} | ${mockLabels} | ${'not empty'} | ${false}
`(
'returns $showList if `state.variant` is "$variant" and `labelsFetchInProgress` is $loading',
({ variant, loading, showList }) => {
createComponent({ ...mockConfig, variant });
wrapper.vm.$store.state.labelsFetchInProgress = loading;
'returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription',
async ({ searchKey, labels, returnValue }) => {
wrapper.setData({
searchKey,
});
expect(wrapper.vm.showListContainer).toBe(showList);
wrapper.vm.$store.dispatch('receiveLabelsSuccess', labels);
await wrapper.vm.$nextTick();
expect(wrapper.vm.showNoMatchingResultsMessage).toBe(returnValue);
},
);
});
......@@ -118,6 +128,28 @@ describe('DropdownContentsLabelsView', () => {
});
});
describe('handleComponentDisappear', () => {
it('calls action `receiveLabelsSuccess` with empty array', () => {
jest.spyOn(wrapper.vm, 'receiveLabelsSuccess');
wrapper.vm.handleComponentDisappear();
expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]);
});
});
describe('handleCreateLabelClick', () => {
it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', () => {
jest.spyOn(wrapper.vm, 'receiveLabelsSuccess');
jest.spyOn(wrapper.vm, 'toggleDropdownContentsCreateView');
wrapper.vm.handleCreateLabelClick();
expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]);
expect(wrapper.vm.toggleDropdownContentsCreateView).toHaveBeenCalled();
});
});
describe('handleKeyDown', () => {
it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => {
wrapper.setData({
......@@ -226,8 +258,8 @@ describe('DropdownContentsLabelsView', () => {
});
describe('template', () => {
it('renders component container element with class `labels-select-contents-list`', () => {
expect(wrapper.attributes('class')).toContain('labels-select-contents-list');
it('renders gl-intersection-observer as component root', () => {
expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
});
it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => {
......@@ -272,15 +304,11 @@ describe('DropdownContentsLabelsView', () => {
expect(searchInputEl.attributes('autofocus')).toBe('true');
});
it('renders smart-virtual-list element', () => {
expect(wrapper.find(SmartVirtualList).exists()).toBe(true);
});
it('renders label elements for all labels', () => {
expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
});
it('renders label element with "is-focused" when value of `currentHighlightItem` is more than -1', () => {
it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', () => {
wrapper.setData({
currentHighlightItem: 0,
});
......@@ -288,7 +316,7 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => {
const labelItemEl = findDropdownContent().find(LabelItem);
expect(labelItemEl.props('highlight')).toBe(true);
expect(labelItemEl.attributes('highlight')).toBe('true');
});
});
......@@ -310,9 +338,12 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => {
const dropdownContent = findDropdownContent();
const loadingIcon = findLoadingIcon();
expect(dropdownContent.exists()).toBe(true);
expect(dropdownContent.isVisible()).toBe(false);
expect(dropdownContent.isVisible()).toBe(true);
expect(loadingIcon.exists()).toBe(true);
expect(loadingIcon.isVisible()).toBe(true);
});
});
......
......@@ -6,11 +6,15 @@ import { mockRegularLabel } from './mock_data';
const mockLabel = { ...mockRegularLabel, set: true };
const createComponent = ({ label = mockLabel, highlight = true } = {}) =>
const createComponent = ({
label = mockLabel,
isLabelSet = mockLabel.set,
highlight = true,
} = {}) =>
shallowMount(LabelItem, {
propsData: {
label,
isLabelSet: label.set,
isLabelSet,
highlight,
},
});
......@@ -26,94 +30,44 @@ describe('LabelItem', () => {
wrapper.destroy();
});
describe('computed', () => {
describe('labelBoxStyle', () => {
it('returns an object containing `backgroundColor` based on `label` prop', () => {
expect(wrapper.vm.labelBoxStyle).toEqual(
expect.objectContaining({
backgroundColor: mockLabel.color,
}),
);
});
});
});
describe('watchers', () => {
describe('isLabelSet', () => {
it('sets value of `isLabelSet` to `isSet` data prop', () => {
expect(wrapper.vm.isSet).toBe(true);
wrapper.setProps({
isLabelSet: false,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.isSet).toBe(false);
});
});
});
});
describe('methods', () => {
describe('handleClick', () => {
it('sets value of `isSet` data prop to opposite of its current value', () => {
wrapper.setData({
isSet: true,
});
wrapper.vm.handleClick();
expect(wrapper.vm.isSet).toBe(false);
wrapper.vm.handleClick();
expect(wrapper.vm.isSet).toBe(true);
});
it('emits event `clickLabel` on component with `label` prop as param', () => {
wrapper.vm.handleClick();
expect(wrapper.emitted('clickLabel')).toBeTruthy();
expect(wrapper.emitted('clickLabel')[0]).toEqual([mockLabel]);
});
});
});
describe('template', () => {
it('renders gl-link component', () => {
expect(wrapper.find(GlLink).exists()).toBe(true);
});
it('renders gl-link component with class `is-focused` when `highlight` prop is true', () => {
wrapper.setProps({
it('renders component root with class `is-focused` when `highlight` prop is true', () => {
const wrapperTemp = createComponent({
highlight: true,
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.find(GlLink).classes()).toContain('is-focused');
});
expect(wrapperTemp.classes()).toContain('is-focused');
wrapperTemp.destroy();
});
it('renders visible gl-icon component when `isSet` prop is true', () => {
wrapper.setData({
isSet: true,
it('renders visible gl-icon component when `isLabelSet` prop is true', () => {
const wrapperTemp = createComponent({
isLabelSet: true,
});
return wrapper.vm.$nextTick(() => {
const iconEl = wrapper.find(GlIcon);
const iconEl = wrapperTemp.find(GlIcon);
expect(iconEl.isVisible()).toBe(true);
expect(iconEl.props('name')).toBe('mobile-issue-close');
});
expect(iconEl.isVisible()).toBe(true);
expect(iconEl.props('name')).toBe('mobile-issue-close');
wrapperTemp.destroy();
});
it('renders visible span element as placeholder instead of gl-icon when `isSet` prop is false', () => {
wrapper.setData({
isSet: false,
it('renders visible span element as placeholder instead of gl-icon when `isLabelSet` prop is false', () => {
const wrapperTemp = createComponent({
isLabelSet: false,
});
return wrapper.vm.$nextTick(() => {
const placeholderEl = wrapper.find('[data-testid="no-icon"]');
const placeholderEl = wrapperTemp.find('[data-testid="no-icon"]');
expect(placeholderEl.isVisible()).toBe(true);
});
expect(placeholderEl.isVisible()).toBe(true);
wrapperTemp.destroy();
});
it('renders label color element', () => {
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20201104124300_ensure_namespace_settings_creation.rb')
RSpec.describe EnsureNamespaceSettingsCreation do
context 'when there are namespaces without namespace settings' do
let(:namespaces) { table(:namespaces) }
let(:namespace_settings) { table(:namespace_settings) }
let!(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
let!(:namespace_2) { namespaces.create!(name: 'gitlab', path: 'gitlab-org2') }
it 'migrates namespaces without namespace_settings' do
stub_const("#{described_class.name}::BATCH_SIZE", 2)
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(2.minutes.to_i, namespace.id, namespace_2.id)
end
end
end
it 'schedules migrations in batches ' do
stub_const("#{described_class.name}::BATCH_SIZE", 2)
namespace_3 = namespaces.create!(name: 'gitlab', path: 'gitlab-org3')
namespace_4 = namespaces.create!(name: 'gitlab', path: 'gitlab-org4')
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(2.minutes.to_i, namespace.id, namespace_2.id)
expect(described_class::MIGRATION)
.to be_scheduled_delayed_migration(4.minutes.to_i, namespace_3.id, namespace_4.id)
end
end
end
end
end
......@@ -30,41 +30,23 @@ RSpec.describe MigrateComplianceFrameworkEnumToDatabaseFrameworkRecord, schema:
subject { described_class.new.up }
context 'when Gitlab.ee? is true' do
before do
expect(Gitlab).to receive(:ee?).and_return(true)
end
it 'updates the project settings' do
subject
it 'updates the project settings' do
subject
gdpr_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'GDPR')
expect(project_on_root_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id)
expect(project_on_sub_sub_level_compliance_setting_2.reload.framework_id).to eq(gdpr_framework.id)
gdpr_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'GDPR')
expect(project_on_root_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id)
expect(project_on_sub_sub_level_compliance_setting_2.reload.framework_id).to eq(gdpr_framework.id)
sox_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'SOX')
expect(project_on_sub_sub_level_compliance_setting_1.reload.framework_id).to eq(sox_framework.id)
sox_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'SOX')
expect(project_on_sub_sub_level_compliance_setting_1.reload.framework_id).to eq(sox_framework.id)
gdpr_framework = compliance_management_frameworks.find_by(namespace_id: namespace.id, name: 'GDPR')
expect(project_on_namespace_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id)
end
it 'adds two framework records' do
subject
expect(compliance_management_frameworks.count).to eq(3)
end
gdpr_framework = compliance_management_frameworks.find_by(namespace_id: namespace.id, name: 'GDPR')
expect(project_on_namespace_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id)
end
context 'when Gitlab.ee? is false' do
before do
expect(Gitlab).to receive(:ee?).and_return(false)
end
it 'does nothing' do
subject
it 'adds two framework records' do
subject
expect(compliance_management_frameworks.count).to eq(0)
end
expect(compliance_management_frameworks.count).to eq(3)
end
end
......@@ -77,6 +77,42 @@ RSpec.describe Projects::HashedStorage::MigrateRepositoryService do
end
end
context 'when exception happens' do
it 'handles OpenSSL::Cipher::CipherError' do
expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
expect { service.execute }.not_to raise_exception
end
it 'ensures rollback when OpenSSL::Cipher::CipherError' do
expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
expect(service).to receive(:rollback_folder_move).and_call_original
service.execute
project.reload
expect(project.legacy_storage?).to be_truthy
expect(project.repository_read_only?).to be_falsey
end
it 'handles Gitlab::Git::CommandError' do
expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
expect { service.execute }.not_to raise_exception
end
it 'ensures rollback when Gitlab::Git::CommandError' do
expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
expect(service).to receive(:rollback_folder_move).and_call_original
service.execute
project.reload
expect(project.legacy_storage?).to be_truthy
expect(project.repository_read_only?).to be_falsey
end
end
context 'when one move fails' do
it 'rollsback repositories to original name' do
allow(service).to receive(:move_repository).and_call_original
......
......@@ -77,6 +77,42 @@ RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab
end
end
context 'when exception happens' do
it 'handles OpenSSL::Cipher::CipherError' do
expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
expect { service.execute }.not_to raise_exception
end
it 'ensures rollback when OpenSSL::Cipher::CipherError' do
expect(project).to receive(:ensure_runners_token).and_raise(OpenSSL::Cipher::CipherError)
expect(service).to receive(:rollback_folder_move).and_call_original
service.execute
project.reload
expect(project.hashed_storage?(:repository)).to be_truthy
expect(project.repository_read_only?).to be_falsey
end
it 'handles Gitlab::Git::CommandError' do
expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
expect { service.execute }.not_to raise_exception
end
it 'ensures rollback when Gitlab::Git::CommandError' do
expect(project).to receive(:write_repository_config).and_raise(Gitlab::Git::CommandError)
expect(service).to receive(:rollback_folder_move).and_call_original
service.execute
project.reload
expect(project.hashed_storage?(:repository)).to be_truthy
expect(project.repository_read_only?).to be_falsey
end
end
context 'when one move fails' do
it 'rolls repositories back to original name' do
allow(service).to receive(:move_repository).and_call_original
......
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