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 @@ ...@@ -2,6 +2,16 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 13.5.3 (2020-11-03)
### Fixed (3 changes) ### Fixed (3 changes)
......
13.5.3 13.5.4
\ No newline at end of file \ No newline at end of file
...@@ -3,5 +3,3 @@ export const DropdownVariant = { ...@@ -3,5 +3,3 @@ export const DropdownVariant = {
Standalone: 'standalone', Standalone: 'standalone',
Embedded: 'embedded', Embedded: 'embedded',
}; };
export const LIST_BUFFER_SIZE = 5;
<script> <script>
import { mapState, mapGetters, mapActions } from 'vuex'; 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 fuzzaldrinPlus from 'fuzzaldrin-plus';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; 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 LabelItem from './label_item.vue';
import { LIST_BUFFER_SIZE } from './constants';
export default { export default {
LIST_BUFFER_SIZE,
components: { components: {
GlIntersectionObserver,
GlLoadingIcon, GlLoadingIcon,
GlButton, GlButton,
GlSearchBoxByType, GlSearchBoxByType,
GlLink, GlLink,
SmartVirtualList,
LabelItem, LabelItem,
}, },
data() { data() {
...@@ -46,15 +48,8 @@ export default { ...@@ -46,15 +48,8 @@ export default {
} }
return this.labels; return this.labels;
}, },
showListContainer() {
if (this.isDropdownVariantSidebar) {
return !this.labelsFetchInProgress;
}
return true;
},
showNoMatchingResultsMessage() { showNoMatchingResultsMessage() {
return !this.labelsFetchInProgress && !this.visibleLabels.length; return Boolean(this.searchKey) && this.visibleLabels.length === 0;
}, },
}, },
watch: { watch: {
...@@ -67,14 +62,12 @@ export default { ...@@ -67,14 +62,12 @@ export default {
} }
}, },
}, },
mounted() {
this.fetchLabels();
},
methods: { methods: {
...mapActions([ ...mapActions([
'toggleDropdownContents', 'toggleDropdownContents',
'toggleDropdownContentsCreateView', 'toggleDropdownContentsCreateView',
'fetchLabels', 'fetchLabels',
'receiveLabelsSuccess',
'updateSelectedLabels', 'updateSelectedLabels',
'toggleDropdownContents', 'toggleDropdownContents',
]), ]),
...@@ -99,6 +92,17 @@ export default { ...@@ -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 * This method enables keyboard navigation support for
* the dropdown. * the dropdown.
...@@ -135,84 +139,75 @@ export default { ...@@ -135,84 +139,75 @@ export default {
</script> </script>
<template> <template>
<div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown"> <gl-intersection-observer @appear="fetchLabels" @disappear="handleComponentDisappear">
<gl-loading-icon <div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
v-if="labelsFetchInProgress" <div
class="labels-fetch-loading position-absolute gl-display-flex gl-align-items-center w-100 h-100" v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
size="md" class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
/> data-testid="dropdown-title"
<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"
> >
<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 <label-item
v-for="(label, index) in visibleLabels"
:key="label.id"
:label="label" :label="label"
:is-label-set="label.set" :is-label-set="label.set"
:highlight="index === currentHighlightItem" :highlight="index === currentHighlightItem"
@clickLabel="handleLabelClick(label)" @clickLabel="handleLabelClick(label)"
/> />
</li> <li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center">
<li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center"> {{ __('No matching results') }}
{{ __('No matching results') }} </li>
</li> </ul>
</smart-virtual-list> </div>
</div> <div
<div v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded" class="dropdown-footer"
class="dropdown-footer" data-testid="dropdown-footer"
data-testid="dropdown-footer" >
> <ul class="list-unstyled">
<ul class="list-unstyled"> <li v-if="allowLabelCreate">
<li v-if="allowLabelCreate"> <gl-link
<gl-link class="gl-display-flex w-100 flex-row text-break-word label-item"
class="gl-display-flex w-100 flex-row text-break-word label-item" @click="handleCreateLabelClick"
@click="toggleDropdownContentsCreateView" >
> {{ footerCreateLabelTitle }}
{{ footerCreateLabelTitle }} </gl-link>
</gl-link> </li>
</li> <li>
<li> <gl-link
<gl-link :href="labelsManagePath"
:href="labelsManagePath" class="gl-display-flex flex-row text-break-word label-item"
class="gl-display-flex flex-row text-break-word label-item" >
> {{ footerManageLabelTitle }}
{{ footerManageLabelTitle }} </gl-link>
</gl-link> </li>
</li> </ul>
</ul> </div>
</div> </div>
</div> </gl-intersection-observer>
</template> </template>
<script> <script>
import { GlIcon, GlLink } from '@gitlab/ui'; import { GlLink, GlIcon } from '@gitlab/ui';
export default { export default {
components: { functional: true,
GlIcon,
GlLink,
},
props: { props: {
label: { label: {
type: Object, type: Object,
...@@ -21,46 +18,65 @@ export default { ...@@ -21,46 +18,65 @@ export default {
default: false, default: false,
}, },
}, },
data() { render(h, { props, listeners }) {
return { const { label, highlight, isLabelSet } = props;
isSet: this.isLabelSet,
}; const labelColorBox = h('span', {
}, class: 'dropdown-label-box',
computed: { style: {
labelBoxStyle() { backgroundColor: label.color,
return { },
backgroundColor: this.label.color, attrs: {
}; 'data-testid': 'label-color-box',
}, },
}, });
watch: {
/** const checkedIcon = h(GlIcon, {
* This watcher assures that if user used class: {
* `Enter` key to set/unset label, changes 'mr-2 align-self-center': true,
* are reflected here too. hidden: !isLabelSet,
*/ },
isLabelSet(value) { props: {
this.isSet = value; name: 'mobile-issue-close',
}, },
}, });
methods: {
handleClick() { const noIcon = h('span', {
this.isSet = !this.isSet; class: {
this.$emit('clickLabel', this.label); '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> </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 { ...@@ -266,7 +266,7 @@ export default {
</dropdown-value> </dropdown-value>
<dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" /> <dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
<dropdown-contents <dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents" v-show="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents" ref="dropdownContents"
/> />
</template> </template>
......
...@@ -1016,6 +1016,23 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { ...@@ -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 { .labels-select-dropdown-contents {
min-height: $dropdown-min-height; min-height: $dropdown-min-height;
max-height: 330px; max-height: 330px;
...@@ -1049,13 +1066,6 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { ...@@ -1049,13 +1066,6 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
.label-item { .label-item {
padding: 8px 20px; padding: 8px 20px;
&:hover,
&.is-focused {
@include dropdown-item-hover;
text-decoration: none;
}
} }
.color-input-container { .color-input-container {
......
...@@ -41,6 +41,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -41,6 +41,7 @@ class Admin::GroupsController < Admin::ApplicationController
if @group.save if @group.save
@group.add_owner(current_user) @group.add_owner(current_user)
@group.create_namespace_settings
redirect_to [:admin, @group], notice: _('Group %{group_name} was successfully created.') % { group_name: @group.name } redirect_to [:admin, @group], notice: _('Group %{group_name} was successfully created.') % { group_name: @group.name }
else else
render "new" render "new"
......
...@@ -21,14 +21,32 @@ module Projects ...@@ -21,14 +21,32 @@ module Projects
project.storage_version = nil project.storage_version = nil
end end
project.repository_read_only = false project.transaction do
project.save!(validate: false) project.save!(validate: false)
project.set_repository_writable!
if result && block_given?
yield
end end
result 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 end
end end
......
...@@ -21,14 +21,32 @@ module Projects ...@@ -21,14 +21,32 @@ module Projects
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository] project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
end end
project.repository_read_only = false project.transaction do
project.save!(validate: false) project.save!(validate: false)
project.set_repository_writable!
if result && block_given?
yield
end end
result 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 end
end end
......
...@@ -52,8 +52,6 @@ class MigrateComplianceFrameworkEnumToDatabaseFrameworkRecord < ActiveRecord::Mi ...@@ -52,8 +52,6 @@ class MigrateComplianceFrameworkEnumToDatabaseFrameworkRecord < ActiveRecord::Mi
end end
def up def up
return unless Gitlab.ee?
TmpComplianceFramework.reset_column_information TmpComplianceFramework.reset_column_information
TmpProjectSettings.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 ...@@ -5,51 +5,29 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference, concepts 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. > 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 Merge request approval rules prevent users from overriding certain settings on the project
level. When configured, only administrators can change these settings on a project level level. When enabled at the instance level, these settings are no longer editable on the
if they are enabled at an instance level. project level.
To enable merge request approval rules for an instance: To enable merge request approval rules for an instance:
1. Navigate to **Admin Area >** **{push-rules}** **Push Rules** and expand **Merge 1. Navigate to **Admin Area >** **{push-rules}** **Push Rules** and expand **Merge
requests approvals**. requests approvals**.
1. Set the required rule. 1. Set the required rule.
1. Click **Save changes**. 1. Click **Save changes**.
GitLab administrators can later override these settings in a project’s settings.
## Available rules ## Available rules
Merge request approval rules that can be set at an instance level are: Merge request approval rules that can be set at an instance level are:
- **Prevent approval of merge requests by merge request author**. Prevents project - **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 - **Prevent approval of merge requests by merge request committers**. Prevents project
maintainers from allowing users to approve merge requests if they have submitted maintainers from allowing users to approve merge requests if they have submitted
any commits to the source branch. any commits to the source branch.
- **Prevent users from modifying merge request approvers list**. Prevents project - **Prevent users from modifying merge request approvers list**. Prevents users from
maintainers from allowing users to modify the approvers list in project settings modifying the approvers list in project settings or in individual merge requests.
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) |
...@@ -6817,9 +6817,6 @@ msgstr "" ...@@ -6817,9 +6817,6 @@ msgstr ""
msgid "Compliance framework (optional)" msgid "Compliance framework (optional)"
msgstr "" msgstr ""
msgid "Compliance frameworks"
msgstr ""
msgid "ComplianceDashboard|created by:" msgid "ComplianceDashboard|created by:"
msgstr "" msgstr ""
...@@ -21738,9 +21735,6 @@ msgstr "" ...@@ -21738,9 +21735,6 @@ msgstr ""
msgid "Registry setup" msgid "Registry setup"
msgstr "" msgstr ""
msgid "Regulate approvals by authors/committers, based on compliance frameworks. Can be changed only at the instance level."
msgstr ""
msgid "Reindexing status" msgid "Reindexing status"
msgstr "" msgstr ""
...@@ -26064,9 +26058,6 @@ 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." 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 "" 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." 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 "" msgstr ""
......
...@@ -25,6 +25,20 @@ RSpec.describe Admin::GroupsController do ...@@ -25,6 +25,20 @@ RSpec.describe Admin::GroupsController do
end end
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 describe 'PUT #members_update' do
let(:group_user) { create(:user) } let(:group_user) { create(:user) }
......
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; 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 { 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 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'; import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
...@@ -88,20 +93,25 @@ describe('DropdownContentsLabelsView', () => { ...@@ -88,20 +93,25 @@ describe('DropdownContentsLabelsView', () => {
}); });
}); });
describe('showListContainer', () => { describe('showNoMatchingResultsMessage', () => {
it.each` it.each`
variant | loading | showList searchKey | labels | labelsDescription | returnValue
${'sidebar'} | ${false} | ${true} ${''} | ${[]} | ${'empty'} | ${false}
${'sidebar'} | ${true} | ${false} ${'bug'} | ${[]} | ${'empty'} | ${true}
${'not-sidebar'} | ${true} | ${true} ${''} | ${mockLabels} | ${'not empty'} | ${false}
${'not-sidebar'} | ${false} | ${true} ${'bug'} | ${mockLabels} | ${'not empty'} | ${false}
`( `(
'returns $showList if `state.variant` is "$variant" and `labelsFetchInProgress` is $loading', 'returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription',
({ variant, loading, showList }) => { async ({ searchKey, labels, returnValue }) => {
createComponent({ ...mockConfig, variant }); wrapper.setData({
wrapper.vm.$store.state.labelsFetchInProgress = loading; 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', () => { ...@@ -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', () => { describe('handleKeyDown', () => {
it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => { it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => {
wrapper.setData({ wrapper.setData({
...@@ -226,8 +258,8 @@ describe('DropdownContentsLabelsView', () => { ...@@ -226,8 +258,8 @@ describe('DropdownContentsLabelsView', () => {
}); });
describe('template', () => { describe('template', () => {
it('renders component container element with class `labels-select-contents-list`', () => { it('renders gl-intersection-observer as component root', () => {
expect(wrapper.attributes('class')).toContain('labels-select-contents-list'); expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true);
}); });
it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => { it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', () => {
...@@ -272,15 +304,11 @@ describe('DropdownContentsLabelsView', () => { ...@@ -272,15 +304,11 @@ describe('DropdownContentsLabelsView', () => {
expect(searchInputEl.attributes('autofocus')).toBe('true'); 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', () => { it('renders label elements for all labels', () => {
expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length); 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({ wrapper.setData({
currentHighlightItem: 0, currentHighlightItem: 0,
}); });
...@@ -288,7 +316,7 @@ describe('DropdownContentsLabelsView', () => { ...@@ -288,7 +316,7 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
const labelItemEl = findDropdownContent().find(LabelItem); const labelItemEl = findDropdownContent().find(LabelItem);
expect(labelItemEl.props('highlight')).toBe(true); expect(labelItemEl.attributes('highlight')).toBe('true');
}); });
}); });
...@@ -310,9 +338,12 @@ describe('DropdownContentsLabelsView', () => { ...@@ -310,9 +338,12 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
const dropdownContent = findDropdownContent(); const dropdownContent = findDropdownContent();
const loadingIcon = findLoadingIcon();
expect(dropdownContent.exists()).toBe(true); 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'; ...@@ -6,11 +6,15 @@ import { mockRegularLabel } from './mock_data';
const mockLabel = { ...mockRegularLabel, set: true }; const mockLabel = { ...mockRegularLabel, set: true };
const createComponent = ({ label = mockLabel, highlight = true } = {}) => const createComponent = ({
label = mockLabel,
isLabelSet = mockLabel.set,
highlight = true,
} = {}) =>
shallowMount(LabelItem, { shallowMount(LabelItem, {
propsData: { propsData: {
label, label,
isLabelSet: label.set, isLabelSet,
highlight, highlight,
}, },
}); });
...@@ -26,94 +30,44 @@ describe('LabelItem', () => { ...@@ -26,94 +30,44 @@ describe('LabelItem', () => {
wrapper.destroy(); 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', () => { describe('template', () => {
it('renders gl-link component', () => { it('renders gl-link component', () => {
expect(wrapper.find(GlLink).exists()).toBe(true); expect(wrapper.find(GlLink).exists()).toBe(true);
}); });
it('renders gl-link component with class `is-focused` when `highlight` prop is true', () => { it('renders component root with class `is-focused` when `highlight` prop is true', () => {
wrapper.setProps({ const wrapperTemp = createComponent({
highlight: true, highlight: true,
}); });
return wrapper.vm.$nextTick(() => { expect(wrapperTemp.classes()).toContain('is-focused');
expect(wrapper.find(GlLink).classes()).toContain('is-focused');
}); wrapperTemp.destroy();
}); });
it('renders visible gl-icon component when `isSet` prop is true', () => { it('renders visible gl-icon component when `isLabelSet` prop is true', () => {
wrapper.setData({ const wrapperTemp = createComponent({
isSet: true, isLabelSet: true,
}); });
return wrapper.vm.$nextTick(() => { const iconEl = wrapperTemp.find(GlIcon);
const iconEl = wrapper.find(GlIcon);
expect(iconEl.isVisible()).toBe(true); expect(iconEl.isVisible()).toBe(true);
expect(iconEl.props('name')).toBe('mobile-issue-close'); 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', () => { it('renders visible span element as placeholder instead of gl-icon when `isLabelSet` prop is false', () => {
wrapper.setData({ const wrapperTemp = createComponent({
isSet: false, isLabelSet: false,
}); });
return wrapper.vm.$nextTick(() => { const placeholderEl = wrapperTemp.find('[data-testid="no-icon"]');
const placeholderEl = wrapper.find('[data-testid="no-icon"]');
expect(placeholderEl.isVisible()).toBe(true); expect(placeholderEl.isVisible()).toBe(true);
});
wrapperTemp.destroy();
}); });
it('renders label color element', () => { 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: ...@@ -30,41 +30,23 @@ RSpec.describe MigrateComplianceFrameworkEnumToDatabaseFrameworkRecord, schema:
subject { described_class.new.up } subject { described_class.new.up }
context 'when Gitlab.ee? is true' do it 'updates the project settings' do
before do subject
expect(Gitlab).to receive(:ee?).and_return(true)
end
it 'updates the project settings' do gdpr_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'GDPR')
subject 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') sox_framework = compliance_management_frameworks.find_by(namespace_id: root_group.id, name: 'SOX')
expect(project_on_root_level_compliance_setting.reload.framework_id).to eq(gdpr_framework.id) expect(project_on_sub_sub_level_compliance_setting_1.reload.framework_id).to eq(sox_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') gdpr_framework = compliance_management_frameworks.find_by(namespace_id: namespace.id, name: 'GDPR')
expect(project_on_sub_sub_level_compliance_setting_1.reload.framework_id).to eq(sox_framework.id) expect(project_on_namespace_level_compliance_setting.reload.framework_id).to eq(gdpr_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
end end
context 'when Gitlab.ee? is false' do it 'adds two framework records' do
before do subject
expect(Gitlab).to receive(:ee?).and_return(false)
end
it 'does nothing' do
subject
expect(compliance_management_frameworks.count).to eq(0) expect(compliance_management_frameworks.count).to eq(3)
end
end end
end end
...@@ -77,6 +77,42 @@ RSpec.describe Projects::HashedStorage::MigrateRepositoryService do ...@@ -77,6 +77,42 @@ RSpec.describe Projects::HashedStorage::MigrateRepositoryService do
end end
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 context 'when one move fails' do
it 'rollsback repositories to original name' do it 'rollsback repositories to original name' do
allow(service).to receive(:move_repository).and_call_original allow(service).to receive(:move_repository).and_call_original
......
...@@ -77,6 +77,42 @@ RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab ...@@ -77,6 +77,42 @@ RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab
end end
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 context 'when one move fails' do
it 'rolls repositories back to original name' do it 'rolls repositories back to original name' do
allow(service).to receive(:move_repository).and_call_original 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