diff options
92 files changed, 1399 insertions, 603 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index c510a6da2d7..2a81c81b0f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -718,7 +718,7 @@ GEM redis-store (>= 1.3, < 2) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) - redis-rack (2.0.3) + redis-rack (2.0.4) rack (>= 1.5, < 3) redis-store (>= 1.2, < 2) redis-rails (5.0.2) diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index a685960d862..0dd0783ce06 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -45,11 +45,9 @@ export default { onLeaveGroup() { this.modalStatus = true; }, - leaveGroup(leaveConfirmed) { + leaveGroup() { this.modalStatus = false; - if (leaveConfirmed) { - eventHub.$emit('leaveGroup', this.group, this.parentGroup); - } + eventHub.$emit('leaveGroup', this.group, this.parentGroup); }, }, }; diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 803dc63d39c..2e42fb6c9a6 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -42,28 +42,28 @@ export default { v-if="isGroup" css-class="number-subgroups" icon-name="folder" - :title="s__('Subgroups')" - :value=item.subgroupCount + :title="__('Subgroups')" + :value="item.subgroupCount" /> <item-stats-value v-if="isGroup" css-class="number-projects" icon-name="bookmark" - :title="s__('Projects')" - :value=item.projectCount + :title="__('Projects')" + :value="item.projectCount" /> <item-stats-value v-if="isGroup" css-class="number-users" icon-name="users" - :title="s__('Members')" - :value=item.memberCount + :title="__('Members')" + :value="item.memberCount" /> <item-stats-value v-if="isProject" css-class="project-stars" icon-name="star" - :value=item.starCount + :value="item.starCount" /> <item-stats-value css-class="item-visibility" diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 6e67e99a70f..d475813c4f7 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -32,10 +32,10 @@ methods: { createNewItem(type) { this.modalType = type; - this.toggleModalOpen(); + this.openModal = true; }, - toggleModalOpen() { - this.openModal = !this.openModal; + hideModal() { + this.openModal = false; }, }, }; @@ -95,7 +95,7 @@ :branch-id="branch" :path="path" :parent="parent" - @toggle="toggleModalOpen" + @hide="hideModal" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index a0650d37690..0312f56efbd 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -43,10 +43,10 @@ type: this.type, }); - this.toggleModalOpen(); + this.hideModal(); }, - toggleModalOpen() { - this.$emit('toggle'); + hideModal() { + this.$emit('hide'); }, }, computed: { @@ -86,7 +86,7 @@ :title="modalTitle" :primary-button-label="buttonLabel" kind="success" - @toggle="toggleModalOpen" + @cancel="hideModal" @submit="createEntryInStore" > <form diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 470db2c9650..979721dcb5a 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -110,7 +110,7 @@ export default { kind="primary" :title="__('Branch has changed')" :text="__('This branch has changed since you started editing. Would you like to create a new branch?')" - @toggle="showNewBranchModal = false" + @cancel="showNewBranchModal = false" @submit="makeCommit(true)" /> <commit-files-list diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue index 37bd9003e96..42d5d709209 100644 --- a/app/assets/javascripts/ide/components/repo_edit_button.vue +++ b/app/assets/javascripts/ide/components/repo_edit_button.vue @@ -50,7 +50,7 @@ export default { kind="warning" :title="__('Are you sure?')" :text="__('Are you sure you want to discard your changes?')" - @toggle="closeDiscardPopup" + @cancel="closeDiscardPopup" @submit="toggleEditMode(true)" /> </div> diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 9280b7f150c..cb6e06ea584 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -64,3 +64,12 @@ export const truncate = (string, maxLength) => `${string.substr(0, (maxLength - export function capitalizeFirstCharacter(text) { return `${text[0].toUpperCase()}${text.slice(1)}`; } + +/** + * Replaces all html tags from a string with the given replacement. + * + * @param {String} string + * @param {*} replace + * @returns {String} + */ +export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 78be6b6e884..36ad618aa46 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,7 +1,7 @@ <script> - import modal from '../../../vue_shared/components/modal.vue'; - import { __, s__, sprintf } from '../../../locale'; - import csrf from '../../../lib/utils/csrf'; + import modal from '~/vue_shared/components/modal.vue'; + import { __, s__, sprintf } from '~/locale'; + import csrf from '~/lib/utils/csrf'; export default { props: { @@ -22,7 +22,6 @@ return { enteredPassword: '', enteredUsername: '', - isOpen: false, }; }, components: { @@ -69,78 +68,58 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), return this.enteredUsername === this.username; }, - onSubmit(status) { - if (status) { - if (!this.canSubmit()) { - return; - } - - this.$refs.form.submit(); - } - - this.toggleOpen(false); - }, - toggleOpen(isOpen) { - this.isOpen = isOpen; + onSubmit() { + this.$refs.form.submit(); }, }, }; </script> <template> - <div> - <modal - v-if="isOpen" - :title="s__('Profiles|Delete your account?')" - :text="text" - :kind="`danger ${!canSubmit() && 'disabled'}`" - :primary-button-label="s__('Profiles|Delete account')" - @toggle="toggleOpen" - @submit="onSubmit"> - - <template slot="body" slot-scope="props"> - <p v-html="props.text"></p> + <modal + id="delete-account-modal" + :title="s__('Profiles|Delete your account?')" + :text="text" + kind="danger" + :primary-button-label="s__('Profiles|Delete account')" + @submit="onSubmit" + :submit-disabled="!canSubmit()"> - <form - ref="form" - :action="actionUrl" - method="post"> + <template slot="body" slot-scope="props"> + <p v-html="props.text"></p> - <input - type="hidden" - name="_method" - value="delete" /> - <input - type="hidden" - name="authenticity_token" - :value="csrfToken" /> + <form + ref="form" + :action="actionUrl" + method="post"> - <p id="input-label" v-html="inputLabel"></p> + <input + type="hidden" + name="_method" + value="delete" /> + <input + type="hidden" + name="authenticity_token" + :value="csrfToken" /> - <input - v-if="confirmWithPassword" - name="password" - class="form-control" - type="password" - v-model="enteredPassword" - aria-labelledby="input-label" /> - <input - v-else - name="username" - class="form-control" - type="text" - v-model="enteredUsername" - aria-labelledby="input-label" /> - </form> - </template> + <p id="input-label" v-html="inputLabel"></p> - </modal> + <input + v-if="confirmWithPassword" + name="password" + class="form-control" + type="password" + v-model="enteredPassword" + aria-labelledby="input-label" /> + <input + v-else + name="username" + class="form-control" + type="text" + v-model="enteredUsername" + aria-labelledby="input-label" /> + </form> + </template> - <button - type="button" - class="btn btn-danger" - @click="toggleOpen(true)"> - {{ s__('Profiles|Delete account') }} - </button> - </div> + </modal> </template> diff --git a/app/assets/javascripts/profile/account/index.js b/app/assets/javascripts/profile/account/index.js index 635056e0eeb..a93bc935dd0 100644 --- a/app/assets/javascripts/profile/account/index.js +++ b/app/assets/javascripts/profile/account/index.js @@ -1,7 +1,12 @@ import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; + import deleteAccountModal from './components/delete_account_modal.vue'; +Vue.use(Translate); + +const deleteAccountButton = document.getElementById('delete-account-button'); const deleteAccountModalEl = document.getElementById('delete-account-modal'); // eslint-disable-next-line no-new new Vue({ @@ -9,6 +14,9 @@ new Vue({ components: { deleteAccountModal, }, + mounted() { + deleteAccountButton.classList.remove('disabled'); + }, render(createElement) { return createElement('delete-account-modal', { props: { diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 3ecc0c2a6e5..4710e70d619 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,6 +1,7 @@ let hasUserDefinedProjectPath = false; -const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { +const deriveProjectPathFromUrl = ($projectImportUrl) => { + const $currentProjectPath = $projectImportUrl.parents('.toggle-import-form').find('#project_path'); if (hasUserDefinedProjectPath) { return; } @@ -21,7 +22,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => { // extract everything after the last slash const pathMatch = /\/([^/]+)$/.exec(importUrl); if (pathMatch) { - $projectPath.val(pathMatch[1]); + $currentProjectPath.val(pathMatch[1]); } }; @@ -96,7 +97,7 @@ const bindEvents = () => { hasUserDefinedProjectPath = $projectPath.val().trim().length > 0; }); - $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath)); + $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl)); }; document.addEventListener('DOMContentLoaded', bindEvents); diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue new file mode 100644 index 00000000000..05e48ed297f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -0,0 +1,46 @@ +<script> + import { __ } from '~/locale'; + /** + * Port of detail_behavior expand button. + * + * @example + * <expand-button> + * <template slot="expanded"> + * Text goes here. + * </template> + * </expand-button> + */ + export default { + name: 'expandButton', + data() { + return { + isCollapsed: true, + }; + }, + computed: { + ariaLabel() { + return __('Click to expand text'); + }, + }, + methods: { + onClick() { + this.isCollapsed = !this.isCollapsed; + }, + }, + }; +</script> +<template> + <span> + <button + type="button" + v-show="isCollapsed" + class="text-expander btn-blank" + :aria-label="ariaLabel" + @click="onClick"> + ... + </button> + <span v-show="!isCollapsed"> + <slot name="expanded"></slot> + </span> + </span> +</template> diff --git a/app/assets/javascripts/vue_shared/components/modal.vue b/app/assets/javascripts/vue_shared/components/modal.vue index 55f466b7b41..00089dfef38 100644 --- a/app/assets/javascripts/vue_shared/components/modal.vue +++ b/app/assets/javascripts/vue_shared/components/modal.vue @@ -3,6 +3,10 @@ export default { name: 'modal', props: { + id: { + type: String, + required: false, + }, title: { type: String, required: false, @@ -62,11 +66,11 @@ export default { }, methods: { - close() { - this.$emit('toggle', false); + emitCancel(event) { + this.$emit('cancel', event); }, - emitSubmit(status) { - this.$emit('submit', status); + emitSubmit(event) { + this.$emit('submit', event); }, }, }; @@ -75,7 +79,9 @@ export default { <template> <div class="modal-open"> <div - class="modal show" + :id="id" + class="modal" + :class="id ? '' : 'show'" role="dialog" tabindex="-1" > @@ -93,7 +99,8 @@ export default { <button type="button" class="close pull-right" - @click="close" + @click="emitCancel($event)" + data-dismiss="modal" aria-label="Close" > <span aria-hidden="true">×</span> @@ -110,7 +117,8 @@ export default { type="button" class="btn pull-left" :class="btnCancelKindClass" - @click="close"> + @click="emitCancel($event)" + data-dismiss="modal"> {{ closeButtonLabel }} </button> <button @@ -119,13 +127,17 @@ export default { class="btn pull-right js-primary-button" :disabled="submitDisabled" :class="btnKindClass" - @click="emitSubmit(true)"> + @click="emitSubmit($event)" + data-dismiss="modal"> {{ primaryButtonLabel }} </button> </div> </div> </div> </div> - <div class="modal-backdrop fade in" /> + <div + v-if="!id" + class="modal-backdrop fade in"> + </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 8053c65d498..16d60bb2876 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -70,7 +70,7 @@ export default { class="recaptcha-modal js-recaptcha-modal" :hide-footer="true" :title="__('Please solve the reCAPTCHA')" - @toggle="close" + @cancel="close" > <div slot="body"> <p> diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 6f609348402..6f229b08c0c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -353,7 +353,7 @@ class ProjectsController < Projects::ApplicationController end def repo_exists? - project.repository_exists? && !project.empty_repo? && project.repo + project.repository_exists? && !project.empty_repo? rescue Gitlab::Git::Repository::NoRepository project.repository.expire_exists_cache diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index ce432ddbfe6..6de9eb89468 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -1,4 +1,6 @@ class LabelsFinder < UnionFinder + include Gitlab::Utils::StrongMemoize + def initialize(current_user, params = {}) @current_user = current_user @params = params @@ -32,6 +34,8 @@ class LabelsFinder < UnionFinder label_ids << project.labels end end + elsif only_group_labels? + label_ids << Label.where(group_id: group.id) else label_ids << Label.where(group_id: projects.group_ids) label_ids << Label.where(project_id: projects.select(:id)) @@ -51,6 +55,13 @@ class LabelsFinder < UnionFinder items.where(title: title) end + def group + strong_memoize(:group) do + group = Group.find(params[:group_id]) + authorized_to_read_labels?(group) && group + end + end + def group? params[:group_id].present? end @@ -63,6 +74,10 @@ class LabelsFinder < UnionFinder params[:project_ids].present? end + def only_group_labels? + params[:only_group_labels] + end + def title params[:title] || params[:name] end @@ -96,9 +111,9 @@ class LabelsFinder < UnionFinder @projects end - def authorized_to_read_labels?(project) + def authorized_to_read_labels?(label_parent) return true if skip_authorization - Ability.allowed?(current_user, :read_label, project) + Ability.allowed?(current_user, :read_label, label_parent) end end diff --git a/app/models/project.rb b/app/models/project.rb index 5d6c1b30587..4cb9d9fe637 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -987,10 +987,6 @@ class Project < ActiveRecord::Base false end - def repo - repository.rugged - end - def url_to_repo gitlab_shell.url_to_repo(full_path) end @@ -1433,7 +1429,7 @@ class Project < ActiveRecord::Base # We'd need to keep track of project full path otherwise directory tree # created with hashed storage enabled cannot be usefully imported using # the import rake task. - repo.config['gitlab.fullpath'] = gl_full_path + repository.rugged.config['gitlab.fullpath'] = gl_full_path rescue Gitlab::Git::Repository::NoRepository => e Rails.logger.error("Error writing to .git/config for project #{full_path} (#{id}): #{e.message}.") nil diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index d2d45e402b0..f0bcba588a2 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -28,12 +28,18 @@ class GroupPolicy < BasePolicy with_options scope: :subject, score: 0 condition(:request_access_enabled) { @subject.request_access_enabled } - rule { public_group } .enable :read_group + rule { public_group }.policy do + enable :read_group + enable :read_list + enable :read_label + end + rule { logged_in_viewable }.enable :read_group rule { guest }.policy do enable :read_group enable :upload_file + enable :read_label end rule { admin } .enable :read_group diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index f1313b79589..79e197ad08b 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -84,11 +84,13 @@ = s_('Profiles|Deleting an account has the following effects:') = render 'users/deletion_guidance', user: current_user + %button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal', + target: '#delete-account-modal' } } + = s_('Profiles|Delete account') + #delete-account-modal{ data: { action_url: user_registration_path, confirm_with_password: ('true' if current_user.confirm_deletion_with_password?), username: current_user.username } } - %button.btn.btn-danger.disabled - = s_('Profiles|Delete account') - else - if @user.solo_owned_groups.present? %p diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml index 7032b892029..8a13713ae02 100644 --- a/app/views/projects/clusters/_advanced_settings.html.haml +++ b/app/views/projects/clusters/_advanced_settings.html.haml @@ -11,5 +11,5 @@ %label.text-danger = s_('ClusterIntegration|Remove cluster integration') %p - = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine.') - = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Kubernetes Engine"}) + = s_("ClusterIntegration|Remove this cluster's configuration from this project. This will not delete your actual cluster.") + = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this cluster's integration? This will not delete your actual cluster.")}) diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml index 76a66fb92a2..26ca3307a4a 100644 --- a/app/views/projects/clusters/_banner.html.haml +++ b/app/views/projects/clusters/_banner.html.haml @@ -1,6 +1,6 @@ -%h4= s_('ClusterIntegration|Enable cluster integration') -.settings-content +%h4= s_('ClusterIntegration|Cluster integration') +.settings-content .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine') %p.js-error-reason @@ -11,11 +11,4 @@ .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } = s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details') - %p - - if @cluster.enabled? - - if can?(current_user, :update_cluster, @cluster) - = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.') - - else - = s_('ClusterIntegration|Cluster integration is enabled for this project.') - - else - = s_('ClusterIntegration|Cluster integration is disabled for this project.') + %p= s_('ClusterIntegration|Control how your cluster integrates with GitLab') diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml index ad696daa259..3943dfc0856 100644 --- a/app/views/projects/clusters/_cluster.html.haml +++ b/app/views/projects/clusters/_cluster.html.haml @@ -4,7 +4,7 @@ .table-mobile-content = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster) .table-section.section-30 - .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment pattern") + .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment scope") .table-mobile-content= cluster.environment_scope .table-section.section-30 .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace") diff --git a/app/views/projects/clusters/_enabled.html.haml b/app/views/projects/clusters/_enabled.html.haml deleted file mode 100644 index 547b3c8446f..00000000000 --- a/app/views/projects/clusters/_enabled.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| - = form_errors(@cluster) - .form-group.append-bottom-20 - %label.append-bottom-10 - = field.hidden_field :enabled, { class: 'js-toggle-input'} - - %button{ type: 'button', - class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", - "aria-label": s_("ClusterIntegration|Toggle Cluster"), - disabled: !can?(current_user, :update_cluster, @cluster) } - %span.toggle-icon - = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') - = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') - - - if can?(current_user, :update_cluster, @cluster) - .form-group - = field.submit _('Save'), class: 'btn btn-success' diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml new file mode 100644 index 00000000000..9d593ffc021 --- /dev/null +++ b/app/views/projects/clusters/_integration_form.html.haml @@ -0,0 +1,33 @@ += form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| + = form_errors(@cluster) + .form-group.append-bottom-20 + %h5= s_('ClusterIntegration|Integration status') + %p + - if @cluster.enabled? + - if can?(current_user, :update_cluster, @cluster) + = s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.') + - else + = s_('ClusterIntegration|Cluster integration is enabled for this project.') + - else + = s_('ClusterIntegration|Cluster integration is disabled for this project.') + %label.append-bottom-10 + = field.hidden_field :enabled, { class: 'js-toggle-input'} + + %button{ type: 'button', + class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", + "aria-label": s_("ClusterIntegration|Toggle Cluster"), + disabled: !can?(current_user, :update_cluster, @cluster) } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + + .form-group + %h5= s_('ClusterIntegration|Environment scope') + %p + = s_("ClusterIntegration|Choose which of your project's environments will use this cluster.") + = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments') + = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') + + - if can?(current_user, :update_cluster, @cluster) + .form-group + = field.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml index bde85aed341..f3122a1bf47 100644 --- a/app/views/projects/clusters/gcp/_show.html.haml +++ b/app/views/projects/clusters/gcp/_show.html.haml @@ -9,10 +9,6 @@ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_errors(@cluster) - .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope') - = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') - = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml index bec512be91c..74dbe859eea 100644 --- a/app/views/projects/clusters/index.html.haml +++ b/app/views/projects/clusters/index.html.haml @@ -13,7 +13,7 @@ .table-section.section-30{ role: "rowheader" } = s_("ClusterIntegration|Cluster") .table-section.section-30{ role: "rowheader" } - = s_("ClusterIntegration|Environment pattern") + = s_("ClusterIntegration|Environment scope") .table-section.section-30{ role: "rowheader" } = s_("ClusterIntegration|Project namespace") .table-section.section-10{ role: "rowheader" } diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index 0115c64c076..c7c84b5a42c 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -18,9 +18,9 @@ .js-cluster-application-notice .flash-container - %section.settings.no-animate.expanded + %section.settings.no-animate.expanded#cluster-integration = render 'banner' - = render 'enabled' + = render 'integration_form' .cluster-applications-table#js-cluster-applications @@ -41,6 +41,6 @@ %h4= _('Advanced settings') %button.btn.js-settings-toggle = expanded ? 'Collapse' : 'Expand' - %p= s_('ClusterIntegration|Manage cluster integration on your GitLab project') + %p= s_("ClusterIntegration|Advanced options on this cluster's integration") .settings-content = render 'advanced_settings' diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml index 89595bca007..5931e0b7f17 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/projects/clusters/user/_show.html.haml @@ -4,10 +4,6 @@ = field.label :name, s_('ClusterIntegration|Cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name') - .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope') - = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') - = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') diff --git a/changelogs/unreleased/40228-verify-integrity-of-repositories.yml b/changelogs/unreleased/40228-verify-integrity-of-repositories.yml new file mode 100644 index 00000000000..261d48652db --- /dev/null +++ b/changelogs/unreleased/40228-verify-integrity-of-repositories.yml @@ -0,0 +1,5 @@ +--- +title: Fix gitlab-rake gitlab:import:repos import schedule +merge_request: 15931 +author: +type: fixed diff --git a/changelogs/unreleased/api-domains-expose-project_id.yml b/changelogs/unreleased/api-domains-expose-project_id.yml new file mode 100644 index 00000000000..22617ffe9b5 --- /dev/null +++ b/changelogs/unreleased/api-domains-expose-project_id.yml @@ -0,0 +1,5 @@ +--- +title: Expose project_id on /api/v4/pages/domains +merge_request: 16200 +author: Luc Didry +type: changed diff --git a/changelogs/unreleased/feature-api_runners_online.yml b/changelogs/unreleased/feature-api_runners_online.yml new file mode 100644 index 00000000000..08f4dd16f28 --- /dev/null +++ b/changelogs/unreleased/feature-api_runners_online.yml @@ -0,0 +1,5 @@ +--- +title: Add online and status attribute to runner api entity +merge_request: 11750 +author: +type: added diff --git a/changelogs/unreleased/jivl-fix-import-project-url-bug.yml b/changelogs/unreleased/jivl-fix-import-project-url-bug.yml new file mode 100644 index 00000000000..0d97b9c9a53 --- /dev/null +++ b/changelogs/unreleased/jivl-fix-import-project-url-bug.yml @@ -0,0 +1,5 @@ +--- +title: Fix import project url not updating project name +merge_request: 16120 +author: +type: fixed diff --git a/changelogs/unreleased/ldap_username_attributes.yml b/changelogs/unreleased/ldap_username_attributes.yml new file mode 100644 index 00000000000..89bbca58fc9 --- /dev/null +++ b/changelogs/unreleased/ldap_username_attributes.yml @@ -0,0 +1,5 @@ +--- +title: Modify `LDAP::Person` to return username value based on attributes +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/update-redis-rack.yml b/changelogs/unreleased/update-redis-rack.yml new file mode 100644 index 00000000000..6e2e6e203b8 --- /dev/null +++ b/changelogs/unreleased/update-redis-rack.yml @@ -0,0 +1,5 @@ +--- +title: Update redis-rack to 2.0.4 +merge_request: +author: +type: other diff --git a/changelogs/unreleased/winh-modal-target-id.yml b/changelogs/unreleased/winh-modal-target-id.yml new file mode 100644 index 00000000000..f8d5b72be50 --- /dev/null +++ b/changelogs/unreleased/winh-modal-target-id.yml @@ -0,0 +1,5 @@ +--- +title: Add id to modal.vue to support data-toggle="modal" +merge_request: 16189 +author: +type: other diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md index c8b5434c068..c39cb49b1c6 100644 --- a/doc/administration/raketasks/check.md +++ b/doc/administration/raketasks/check.md @@ -28,19 +28,25 @@ exactly which repositories are causing the trouble. ### Check all GitLab repositories +>**Note:** +> +> - `gitlab:repo:check` has been deprecated in favor of `gitlab:git:fsck` +> - [Deprecated][ce-15931] in GitLab 10.4. +> - `gitlab:repo:check` will be removed in the future. [Removal issue][ce-41699] + This task loops through all repositories on the GitLab server and runs the 3 integrity checks described previously. **Omnibus Installation** ``` -sudo gitlab-rake gitlab:repo:check +sudo gitlab-rake gitlab:git:fsck ``` **Source Installation** ```bash -sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production +sudo -u git -H bundle exec rake gitlab:git:fsck RAILS_ENV=production ``` ### Check repositories for a specific user @@ -76,3 +82,6 @@ The LDAP check Rake task will test the bind_dn and password credentials (if configured) and will list a sample of LDAP users. This task is also executed as part of the `gitlab:check` task, but can run independently. See [LDAP Rake Tasks - LDAP Check](ldap.md#check) for details. + +[ce-15931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15931 +[ce-41699]: https://gitlab.com/gitlab-org/gitlab-ce/issues/41699 diff --git a/doc/api/boards.md b/doc/api/boards.md index 69c47abc806..246de50323e 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -15,10 +15,10 @@ GET /projects/:id/boards | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards ``` Example response: @@ -27,6 +27,19 @@ Example response: [ { "id" : 1, + "project": { + "id": 5, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site" + }, + "milestone": { + "id": 12 + "title": "10.0" + }, "lists" : [ { "id" : 1, @@ -60,6 +73,74 @@ Example response: ] ``` +## Single board + +Get a single board. + +``` +GET /projects/:id/boards/:board_id +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1 +``` + +Example response: + +```json + { + "id": 1, + "name:": "project issue board", + "project": { + "id": 5, + "name": "Diaspora Project Site", + "name_with_namespace": "Diaspora / Diaspora Project Site", + "path": "diaspora-project-site", + "path_with_namespace": "diaspora/diaspora-project-site", + "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git", + "web_url": "http://example.com/diaspora/diaspora-project-site" + }, + "milestone": { + "id": 12 + "title": "10.0" + }, + "lists" : [ + { + "id" : 1, + "label" : { + "name" : "Testing", + "color" : "#F0AD4E", + "description" : null + }, + "position" : 1 + }, + { + "id" : 2, + "label" : { + "name" : "Ready", + "color" : "#FF0000", + "description" : null + }, + "position" : 2 + }, + { + "id" : 3, + "label" : { + "name" : "Production", + "color" : "#FF5F00", + "description" : null + }, + "position" : 3 + } + ] + } +``` + ## List board lists Get a list of the board's lists. @@ -71,8 +152,8 @@ GET /projects/:id/boards/:board_id/lists | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists @@ -122,9 +203,9 @@ GET /projects/:id/boards/:board_id/lists/:list_id | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `list_id`| integer | yes | The ID of a board's list | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `list_id`| integer | yes | The ID of a board's list | ```bash curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1 @@ -154,9 +235,9 @@ POST /projects/:id/boards/:board_id/lists | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `label_id` | integer | yes | The ID of a label | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `label_id` | integer | yes | The ID of a label | ```bash curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5 @@ -186,10 +267,10 @@ PUT /projects/:id/boards/:board_id/lists/:list_id | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `list_id` | integer | yes | The ID of a board's list | -| `position` | integer | yes | The position of the list | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `list_id` | integer | yes | The ID of a board's list | +| `position` | integer | yes | The position of the list | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2 @@ -219,9 +300,9 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `board_id` | integer | yes | The ID of a board | -| `list_id` | integer | yes | The ID of a board's list | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `board_id` | integer | yes | The ID of a board | +| `list_id` | integer | yes | The ID of a board's list | ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1 diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md index 50685f335f7..20275b902c6 100644 --- a/doc/api/pages_domains.md +++ b/doc/api/pages_domains.md @@ -21,6 +21,7 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a { "domain": "ssl.domain.example", "url": "https://ssl.domain.example", + "project_id": 1337, "certificate": { "expired": false, "expiration": "2020-04-12T14:32:00.000Z" diff --git a/doc/api/runners.md b/doc/api/runners.md index 015b09a745e..7495c6cdedb 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -30,14 +30,18 @@ Example response: "description": "test-1-20150125", "id": 6, "is_shared": false, - "name": null + "name": null, + "online": true, + "status": "online" }, { "active": true, "description": "test-2-20150125", "id": 8, "is_shared": false, - "name": null + "name": null, + "online": false, + "status": "offline" } ] ``` @@ -69,28 +73,36 @@ Example response: "description": "shared-runner-1", "id": 1, "is_shared": true, - "name": null + "name": null, + "online": true, + "status": "online" }, { "active": true, "description": "shared-runner-2", "id": 3, "is_shared": true, - "name": null + "name": null, + "online": false + "status": "offline" }, { "active": true, "description": "test-1-20150125", "id": 6, "is_shared": false, - "name": null + "name": null, + "online": true + "status": "paused" }, { "active": true, "description": "test-2-20150125", "id": 8, "is_shared": false, - "name": null + "name": null, + "online": false, + "status": "offline" } ] ``` @@ -122,6 +134,8 @@ Example response: "is_shared": false, "contacted_at": "2016-01-25T16:39:48.066Z", "name": null, + "online": true, + "status": "online", "platform": null, "projects": [ { @@ -176,6 +190,8 @@ Example response: "is_shared": false, "contacted_at": "2016-01-25T16:39:48.066Z", "name": null, + "online": true, + "status": "online", "platform": null, "projects": [ { @@ -327,14 +343,18 @@ Example response: "description": "test-2-20150125", "id": 8, "is_shared": false, - "name": null + "name": null, + "online": false, + "status": "offline" }, { "active": true, "description": "development_runner", "id": 5, "is_shared": true, - "name": null + "name": null, + "online": true + "status": "paused" } ] ``` @@ -364,7 +384,9 @@ Example response: "description": "test-2016-02-01", "id": 9, "is_shared": false, - "name": null + "name": null, + "online": true, + "status": "online" } ``` diff --git a/doc/api/settings.md b/doc/api/settings.md index 0e4758cda2d..0b5b1f0c134 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -69,7 +69,7 @@ PUT /application/settings | `after_sign_up_text` | string | no | Text shown to the user after signing up | | `akismet_api_key` | string | no | API key for akismet spam protection | | `akismet_enabled` | boolean | no | Enable or disable akismet spam protection | -| `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. | +| `circuitbreaker_access_retries` | integer | no | The number of attempts GitLab will make to access a storage. | | `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. | | `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. | | `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. | diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md index ca2048c7019..26abf967dcf 100644 --- a/doc/development/gitaly.md +++ b/doc/development/gitaly.md @@ -97,6 +97,29 @@ describe 'Gitaly Request count tests' do end ``` +## Running tests with a locally modified version of Gitaly + +Normally, gitlab-ce/ee tests use a local clone of Gitaly in `tmp/tests/gitaly` +pinned at the version specified in GITALY_SERVER_VERSION. If you want +to run tests locally against a modified version of Gitaly you can +replace `tmp/tests/gitaly` with a symlink. + +```shell +rm -rf tmp/tests/gitaly +ln -s /path/to/gitaly tmp/tests/gitaly +``` + +Make sure you run `make` in your local Gitaly directory before running +tests. Otherwise, Gitaly will fail to boot. + +If you make changes to your local Gitaly in between test runs you need +to manually run `make` again. + +Note that CI tests will not use your locally modified version of +Gitaly. To use a custom Gitaly version in CI you need to update +GITALY_SERVER_VERSION. You can use the format `=revision` to use a +non-tagged commit from https://gitlab.com/gitlab-org/gitaly in CI. + --- [Return to Development documentation](README.md) diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md new file mode 100644 index 00000000000..abe5b06e0f0 --- /dev/null +++ b/doc/development/testing_guide/end_to_end_tests.md @@ -0,0 +1,80 @@ +# End-to-End Testing + +## What is End-to-End testing? + +End-to-End testing is a strategy used to check whether your application works +as expected across entire software stack and architecture, including +integration of all microservices and components that are supposed to work +together. + +## How do we test GitLab? + +We use [Omnibus GitLab][omnibus-gitlab] to build GitLab packages and then we +test these packages using [GitLab QA][gitlab-qa] project, which is entirely +black-box, click-driven testing framework. + +### Testing nightly builds + +We run scheduled pipeline each night to test nightly builds created by Omnibus. +You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pipelines]. + +### Testing code in merge requests + +It is possible to run end-to-end tests (eventually being run within a +[GitLab QA pipeline][gitlab-qa-pipelines]) for a merge request by triggering +the `package-qa` manual action, that should be present in a merge request +widget. + +Mmanual action that starts end-to-end tests is also available in merge requests +in Omnibus GitLab project. + +Below you can read more about how to use it and how does it work. + +#### How does it work? + +Currently, we are using _multi-project pipeline_-like approach to run QA +pipelines. + +1. Developer triggers a manual action, that can be found in CE and EE merge +requests. This starts a chain of pipelines in multiple projects. + +1. The script being executed triggers a pipeline in GitLab Omnibus and waits +for the resulting status. We call this a _status attribution_. + +1. GitLab packages are being built in Omnibus pipeline. Packages are going to be +pushed to Container Registry. + +1. When packages are ready, and available in the registry, a final step in the +pipeline, that is now running in Omnibus, triggers a new pipeline in the GitLab +QA project. It also waits for a resulting status. + +1. GitLab QA pulls images from the registry, spins-up containers and runs tests +against a test environment that has been just orchestrated by the `gitlab-qa` +tool. + +1. The result of the GitLab QA pipeline is being propagated upstream, through +Omnibus, back to CE / EE merge request. + +#### How do I write tests? + +In order to write new tests, you first need to learn more about GitLab QA +architecture. See the [documentation about it][gitlab-qa-architecture] in +GitLab QA project. + +Once you decided where to put test environment orchestration scenarios and +instance specs, take a look at the [relevant documentation][instance-qa-readme] +and examples in [the `qa/` directory][instance-qa-examples]. + +## Where can I ask for help? + +You can ask question in the `#qa` channel on Slack (GitLab internal) or you can +find an issue you would like to work on in [the issue tracker][gitlab-qa-issues] +and start a new discussion there. + +[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab +[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa +[gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines +[gitlab-qa-architecture]: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/architecture.md +[gitlab-qa-issues]: https://gitlab.com/gitlab-org/gitlab-qa/issues +[instance-qa-readme]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/README.md +[instance-qa-examples]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 65386f231a0..74d09eb91ff 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -65,6 +65,13 @@ Everything you should know about how to test Rake tasks. --- +## [End-to-end tests](end_to_end_tests.md) + +Everything you should know about how to run end-to-end tests using +[GitLab QA][gitlab-qa] testing framework. + +--- + ## Spinach (feature) tests GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) @@ -89,3 +96,4 @@ test should be re-implemented using RSpec instead. [Capybara]: https://github.com/teamcapybara/capybara [Karma]: http://karma-runner.github.io/ [Jasmine]: https://jasmine.github.io/ +[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index 1cbd4350284..4adf0dc7c7a 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -121,6 +121,9 @@ running feature tests (i.e. using Capybara) against it. The actual test scenarios and steps are [part of GitLab Rails] so that they're always in-sync with the codebase. +Read a separate document about [end-to-end tests](end_to_end_tests.md) to +learn more. + [multiple pieces]: ../architecture.md#components [GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell [GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse diff --git a/lib/api/api.rb b/lib/api/api.rb index 8094597d238..e0d14281c96 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -119,6 +119,7 @@ module API mount ::API::Features mount ::API::Files mount ::API::Groups + mount ::API::GroupMilestones mount ::API::Internal mount ::API::Issues mount ::API::Jobs @@ -129,8 +130,6 @@ module API mount ::API::Members mount ::API::MergeRequestDiffs mount ::API::MergeRequests - mount ::API::ProjectMilestones - mount ::API::GroupMilestones mount ::API::Namespaces mount ::API::Notes mount ::API::NotificationSettings @@ -139,6 +138,7 @@ module API mount ::API::PipelineSchedules mount ::API::ProjectHooks mount ::API::Projects + mount ::API::ProjectMilestones mount ::API::ProjectSnippets mount ::API::ProtectedBranches mount ::API::Repositories diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 366b0dc9a6f..6c706b2b4e1 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -1,45 +1,46 @@ module API class Boards < Grape::API + include BoardsResponses include PaginationParams before { authenticate! } + helpers do + def board_parent + user_project + end + end + params do requires :id, type: String, desc: 'The ID of a project' end resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do - desc 'Get all project boards' do - detail 'This feature was introduced in 8.13' - success Entities::Board - end - params do - use :pagination - end - get ':id/boards' do - authorize!(:read_board, user_project) - present paginate(user_project.boards), with: Entities::Board + segment ':id/boards' do + desc 'Get all project boards' do + detail 'This feature was introduced in 8.13' + success Entities::Board + end + params do + use :pagination + end + get '/' do + authorize!(:read_board, user_project) + present paginate(board_parent.boards), with: Entities::Board + end + + desc 'Find a project board' do + detail 'This feature was introduced in 10.4' + success Entities::Board + end + get '/:board_id' do + present board, with: Entities::Board + end end params do requires :board_id, type: Integer, desc: 'The ID of a board' end segment ':id/boards/:board_id' do - helpers do - def project_board - board = user_project.boards.first - - if params[:board_id] == board.id - board - else - not_found!('Board') - end - end - - def board_lists - project_board.lists.destroyable - end - end - desc 'Get the lists of a project board' do detail 'Does not include `done` list. This feature was introduced in 8.13' success Entities::List @@ -72,22 +73,13 @@ module API requires :label_id, type: Integer, desc: 'The ID of an existing label' end post '/lists' do - unless available_labels.exists?(params[:label_id]) + unless available_labels_for(user_project).exists?(params[:label_id]) render_api_error!({ error: 'Label not found!' }, 400) end authorize!(:admin_list, user_project) - service = ::Boards::Lists::CreateService.new(user_project, current_user, - { label_id: params[:label_id] }) - - list = service.execute(project_board) - - if list.valid? - present list, with: Entities::List - else - render_validation_error!(list) - end + create_list end desc 'Moves a board list to a new position' do @@ -99,18 +91,11 @@ module API requires :position, type: Integer, desc: 'The position of the list' end put '/lists/:list_id' do - list = project_board.lists.movable.find(params[:list_id]) + list = board_lists.find(params[:list_id]) authorize!(:admin_list, user_project) - service = ::Boards::Lists::MoveService.new(user_project, current_user, - { position: params[:position] }) - - if service.execute(list) - present list, with: Entities::List - else - render_api_error!({ error: "List could not be moved!" }, 400) - end + move_list(list) end desc 'Delete a board list' do @@ -124,12 +109,7 @@ module API authorize!(:admin_list, user_project) list = board_lists.find(params[:list_id]) - destroy_conditionally!(list) do |list| - service = ::Boards::Lists::DestroyService.new(user_project, current_user) - unless service.execute(list) - render_api_error!({ error: 'List could not be deleted!' }, 400) - end - end + destroy_list(list) end end end diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb new file mode 100644 index 00000000000..ead0943a74d --- /dev/null +++ b/lib/api/boards_responses.rb @@ -0,0 +1,50 @@ +module API + module BoardsResponses + extend ActiveSupport::Concern + + included do + helpers do + def board + board_parent.boards.find(params[:board_id]) + end + + def board_lists + board.lists.destroyable + end + + def create_list + create_list_service = + ::Boards::Lists::CreateService.new(board_parent, current_user, { label_id: params[:label_id] }) + + list = create_list_service.execute(board) + + if list.valid? + present list, with: Entities::List + else + render_validation_error!(list) + end + end + + def move_list(list) + move_list_service = + ::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i }) + + if move_list_service.execute(list) + present list, with: Entities::List + else + render_api_error!({ error: "List could not be moved!" }, 400) + end + end + + def destroy_list(list) + destroy_conditionally!(list) do |list| + service = ::Boards::Lists::DestroyService.new(board_parent, current_user) + unless service.execute(list) + render_api_error!({ error: 'List could not be deleted!' }, 400) + end + end + end + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 4ad4a1f7867..bd0c54a1b04 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -791,6 +791,8 @@ module API class Board < Grape::Entity expose :id + expose :project, using: Entities::BasicProjectDetails + expose :lists, using: Entities::List do |board| board.lists.destroyable end @@ -862,6 +864,8 @@ module API expose :active expose :is_shared expose :name + expose :online?, as: :online + expose :status end class RunnerDetails < Runner @@ -1133,6 +1137,7 @@ module API class PagesDomainBasic < Grape::Entity expose :domain expose :url + expose :project_id expose :certificate, as: :certificate_expiration, if: ->(pages_domain, _) { pages_domain.certificate? }, diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 8ad4b2ecbf3..bf388163ec8 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -74,8 +74,15 @@ module API page || not_found!('Wiki Page') end - def available_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute + def available_labels_for(label_parent) + search_params = + if label_parent.is_a?(Project) + { project_id: label_parent.id } + else + { group_id: label_parent.id, only_group_labels: true } + end + + LabelsFinder.new(current_user, search_params).execute end def find_user(id) @@ -141,7 +148,9 @@ module API end def find_project_label(id) - label = available_labels.find_by_id(id) || available_labels.find_by_title(id) + labels = available_labels_for(user_project) + label = labels.find_by_id(id) || labels.find_by_title(id) + label || not_found!('Label') end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index e41a1720ac1..81eaf56e48e 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -15,7 +15,7 @@ module API use :pagination end get ':id/labels' do - present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project + present paginate(available_labels_for(user_project)), with: Entities::Label, current_user: current_user, project: user_project end desc 'Create a new label' do @@ -30,7 +30,7 @@ module API post ':id/labels' do authorize! :admin_label, user_project - label = available_labels.find_by(title: params[:name]) + label = available_labels_for(user_project).find_by(title: params[:name]) conflict!('Label already exists') if label priority = params.delete(:priority) diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb index bd5eb2175e8..4157462ec2a 100644 --- a/lib/api/v3/labels.rb +++ b/lib/api/v3/labels.rb @@ -11,7 +11,7 @@ module API success ::API::Entities::Label end get ':id/labels' do - present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project + present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project end desc 'Delete an existing label' do diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 228d97a87ab..a1755143abe 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -50,10 +50,19 @@ module Gitlab # to the caller to limit the number of blobs and blob_size_limit. # # Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/798 - def batch(repository, blob_references, blob_size_limit: nil) - blob_size_limit ||= MAX_DATA_DISPLAY_SIZE - blob_references.map do |sha, path| - find_by_rugged(repository, sha, path, limit: blob_size_limit) + def batch(repository, blob_references, blob_size_limit: MAX_DATA_DISPLAY_SIZE) + Gitlab::GitalyClient.migrate(:list_blobs_by_sha_path) do |is_enabled| + if is_enabled + Gitlab::GitalyClient.allow_n_plus_1_calls do + blob_references.map do |sha, path| + find_by_gitaly(repository, sha, path, limit: blob_size_limit) + end + end + else + blob_references.map do |sha, path| + find_by_rugged(repository, sha, path, limit: blob_size_limit) + end + end end end @@ -122,13 +131,23 @@ module Gitlab ) end - def find_by_gitaly(repository, sha, path) + def find_by_gitaly(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE) path = path.sub(/\A\/*/, '') path = '/' if path.empty? name = File.basename(path) - entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, MAX_DATA_DISPLAY_SIZE) + + # Gitaly will think that setting the limit to 0 means unlimited, while + # the client might only need the metadata and thus set the limit to 0. + # In this method we'll then set the limit to 1, but clear the byte of data + # that we got back so for the outside world it looks like the limit was + # actually 0. + req_limit = limit == 0 ? 1 : limit + + entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, req_limit) return unless entry + entry.data = "" if limit == 0 + case entry.type when :COMMIT new( diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index aec85f971ca..17c05c44d7e 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1219,9 +1219,16 @@ module Gitlab rebase_path = worktree_path(REBASE_WORKTREE_PREFIX, rebase_id) env = git_env_for_user(user) + if remote_repository.is_a?(RemoteRepository) + env.merge!(remote_repository.fetch_env) + remote_repo_path = GITALY_INTERNAL_URL + else + remote_repo_path = remote_repository.path + end + with_worktree(rebase_path, branch, env: env) do run_git!( - %W(pull --rebase #{remote_repository.path} #{remote_branch}), + %W(pull --rebase #{remote_repo_path} #{remote_branch}), chdir: rebase_path, env: env ) diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 0135b3c6f22..dd5d35feab9 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -15,6 +15,11 @@ module Gitlab execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all)) end + def git_clone_bundle(repo_path:, bundle_path:) + execute(%W(#{git_bin_path} clone --bare -- #{bundle_path} #{repo_path})) + Gitlab::Git::Repository.create_hooks(repo_path, File.expand_path(Gitlab.config.gitlab_shell.hooks_path)) + end + def mkdir_p(path) FileUtils.mkdir_p(path, mode: DEFAULT_MODE) FileUtils.chmod(DEFAULT_MODE, path) diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 32ca2809b2f..d0e5cfcfd3e 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -13,7 +13,7 @@ module Gitlab def restore return true unless File.exist?(@path_to_bundle) - gitlab_shell.import_repository(@project.repository_storage_path, @project.disk_path, @path_to_bundle) + git_clone_bundle(repo_path: @project.repository.path_to_repo, bundle_path: @path_to_bundle) rescue => e @shared.error(e) false diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb index 0afaa2306b5..76863e77dc3 100644 --- a/lib/gitlab/ldap/adapter.rb +++ b/lib/gitlab/ldap/adapter.rb @@ -74,7 +74,7 @@ module Gitlab def user_options(fields, value, limit) options = { - attributes: Gitlab::LDAP::Person.ldap_attributes(config).compact.uniq, + attributes: Gitlab::LDAP::Person.ldap_attributes(config), base: config.base } diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb index c8f19cd52d5..0d9a554fc18 100644 --- a/lib/gitlab/ldap/config.rb +++ b/lib/gitlab/ldap/config.rb @@ -148,7 +148,7 @@ module Gitlab def default_attributes { - 'username' => %w(uid userid sAMAccountName), + 'username' => %w(uid sAMAccountName userid), 'email' => %w(mail email userPrincipalName), 'name' => 'cn', 'first_name' => 'givenName', diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 38d7a9ba2f5..e81cec6ba1a 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -6,6 +6,8 @@ module Gitlab # Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/ AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2") + InvalidEntryError = Class.new(StandardError) + attr_accessor :entry, :provider def self.find_by_uid(uid, adapter) @@ -29,11 +31,12 @@ module Gitlab def self.ldap_attributes(config) [ - 'dn', # Used in `dn` - config.uid, # Used in `uid` - *config.attributes['name'], # Used in `name` - *config.attributes['email'] # Used in `email` - ] + 'dn', + config.uid, + *config.attributes['name'], + *config.attributes['email'], + *config.attributes['username'] + ].compact.uniq end def self.normalize_dn(dn) @@ -60,6 +63,8 @@ module Gitlab Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" } @entry = entry @provider = provider + + validate_entry end def name @@ -71,7 +76,13 @@ module Gitlab end def username - uid + username = attribute_value(:username) + + # Depending on the attribute, multiple values may + # be returned. We need only one for username. + # Ex. `uid` returns only one value but `mail` may + # return an array of multiple email addresses. + [username].flatten.first end def email @@ -104,6 +115,19 @@ module Gitlab entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend end + + def validate_entry + allowed_attrs = self.class.ldap_attributes(config).map(&:downcase) + + # Net::LDAP::Entry transforms keys to symbols. Change to strings to compare. + entry_attrs = entry.attribute_names.map { |n| n.to_s.downcase } + invalid_attrs = entry_attrs - allowed_attrs + + if invalid_attrs.any? + raise InvalidEntryError, + "#{self.class.name} initialized with Net::LDAP::Entry containing invalid attributes(s): #{invalid_attrs}" + end + end end end end diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb new file mode 100644 index 00000000000..d01213bb6e0 --- /dev/null +++ b/lib/gitlab/setup_helper.rb @@ -0,0 +1,61 @@ +module Gitlab + module SetupHelper + class << self + # We cannot create config.toml files for all possible Gitaly configuations. + # For instance, if Gitaly is running on another machine then it makes no + # sense to write a config.toml file on the current machine. This method will + # only generate a configuration for the most common and simplest case: when + # we have exactly one Gitaly process and we are sure it is running locally + # because it uses a Unix socket. + # For development and testing purposes, an extra storage is added to gitaly, + # which is not known to Rails, but must be explicitly stubbed. + def gitaly_configuration_toml(gitaly_dir, gitaly_ruby: true) + storages = [] + address = nil + + Gitlab.config.repositories.storages.each do |key, val| + if address + if address != val['gitaly_address'] + raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address." + end + elsif URI(val['gitaly_address']).scheme != 'unix' + raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses." + else + address = val['gitaly_address'] + end + + storages << { name: key, path: val['path'] } + end + + if Rails.env.test? + storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s } + end + + config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages } + config[:auth] = { token: 'secret' } if Rails.env.test? + config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby + config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path } + config[:bin_dir] = Gitlab.config.gitaly.client_path + + TOML.dump(config) + end + + # rubocop:disable Rails/Output + def create_gitaly_configuration(dir, force: false) + config_path = File.join(dir, 'config.toml') + FileUtils.rm_f(config_path) if force + + File.open(config_path, File::WRONLY | File::CREAT | File::EXCL) do |f| + f.puts gitaly_configuration_toml(dir) + end + rescue Errno::EEXIST + puts "Skipping config.toml generation:" + puts "A configuration file already exists." + rescue ArgumentError => e + puts "Skipping config.toml generation:" + puts e.message + end + # rubocop:enable Rails/Output + end + end +end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 40650fc5ee7..564047bbd34 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -71,7 +71,6 @@ module Gitlab # Ex. # add_repository("/path/to/storage", "gitlab/gitlab-ci") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def add_repository(storage, name) relative_path = name.dup relative_path << '.git' unless relative_path.end_with?('.git') @@ -100,8 +99,12 @@ module Gitlab # Ex. # import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/874 def import_repository(storage, name, url) + if url.start_with?('.', '/') + raise Error.new("don't use disk paths with import_repository: #{url.inspect}") + end + # The timeout ensures the subprocess won't hang forever cmd = gitlab_projects(storage, "#{name}.git") success = cmd.import_project(url, git_timeout) @@ -122,7 +125,6 @@ module Gitlab # Ex. # fetch_remote(my_repo, "upstream") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false) gitaly_migrate(:fetch_remote) do |is_enabled| if is_enabled @@ -142,7 +144,7 @@ module Gitlab # Ex. # mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873 def mv_repository(storage, path, new_path) gitlab_projects(storage, "#{path}.git").mv_project("#{new_path}.git") end @@ -156,7 +158,7 @@ module Gitlab # Ex. # fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "new-namespace/gitlab-ci") # - # Gitaly note: JV: not easy to migrate because this involves two Gitaly servers, not one. + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/817 def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path) gitlab_projects(forked_from_storage, "#{forked_from_disk_path}.git") .fork_repository(forked_to_storage, "#{forked_to_disk_path}.git") @@ -170,7 +172,7 @@ module Gitlab # Ex. # remove_repository("/path/to/storage", "gitlab/gitlab-ci") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 + # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/873 def remove_repository(storage, name) gitlab_projects(storage, "#{name}.git").rm_project end @@ -221,7 +223,6 @@ module Gitlab # Ex. # add_namespace("/path/to/storage", "gitlab") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385 def add_namespace(storage, name) Gitlab::GitalyClient.migrate(:add_namespace) do |enabled| if enabled @@ -243,7 +244,6 @@ module Gitlab # Ex. # rm_namespace("/path/to/storage", "gitlab") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385 def rm_namespace(storage, name) Gitlab::GitalyClient.migrate(:remove_namespace) do |enabled| if enabled @@ -261,7 +261,6 @@ module Gitlab # Ex. # mv_namespace("/path/to/storage", "gitlab", "gitlabhq") # - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/385 def mv_namespace(storage, old_name, new_name) Gitlab::GitalyClient.migrate(:rename_namespace) do |enabled| if enabled diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index dfade1f3885..903e84359cd 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -387,14 +387,8 @@ namespace :gitlab do namespace :repo do desc "GitLab | Check the integrity of the repositories managed by GitLab" task check: :environment do - Gitlab.config.repositories.storages.each do |name, repository_storage| - namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*')) - - namespace_dirs.each do |namespace_dir| - repo_dirs = Dir.glob(File.join(namespace_dir, '*')) - repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) } - end - end + puts "This task is deprecated. Please use gitlab:git:fsck instead".color(:red) + Rake::Task["gitlab:git:fsck"].execute end end @@ -461,35 +455,4 @@ namespace :gitlab do puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".color(:red) end end - - def check_repo_integrity(repo_dir) - puts "\nChecking repo at #{repo_dir.color(:yellow)}" - - git_fsck(repo_dir) - check_config_lock(repo_dir) - check_ref_locks(repo_dir) - end - - def git_fsck(repo_dir) - puts "Running `git fsck`".color(:yellow) - system(*%W(#{Gitlab.config.git.bin_path} fsck), chdir: repo_dir) - end - - def check_config_lock(repo_dir) - config_exists = File.exist?(File.join(repo_dir, 'config.lock')) - config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green) - puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}" - end - - def check_ref_locks(repo_dir) - lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock')) - if lock_files.present? - puts "Ref lock files exist:".color(:red) - lock_files.each do |lock_file| - puts " #{lock_file}" - end - else - puts "No ref lock files exist".color(:green) - end - end end diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake index cf82134d97e..3f5dd2ae3b3 100644 --- a/lib/tasks/gitlab/git.rake +++ b/lib/tasks/gitlab/git.rake @@ -30,6 +30,20 @@ namespace :gitlab do end end + desc 'GitLab | Git | Check all repos integrity' + task fsck: :environment do + failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") do |repo| + check_config_lock(repo) + check_ref_locks(repo) + end + + if failures.empty? + puts "Done".color(:green) + else + output_failures(failures) + end + end + def perform_git_cmd(cmd, message) puts "Starting #{message} on all repositories" @@ -40,6 +54,8 @@ namespace :gitlab do else failures << repo end + + yield(repo) if block_given? end failures @@ -49,5 +65,24 @@ namespace :gitlab do puts "The following repositories reported errors:".color(:red) failures.each { |f| puts "- #{f}" } end + + def check_config_lock(repo_dir) + config_exists = File.exist?(File.join(repo_dir, 'config.lock')) + config_output = config_exists ? 'yes'.color(:red) : 'no'.color(:green) + + puts "'config.lock' file exists?".color(:yellow) + " ... #{config_output}" + end + + def check_ref_locks(repo_dir) + lock_files = Dir.glob(File.join(repo_dir, 'refs/heads/*.lock')) + + if lock_files.present? + puts "Ref lock files exist:".color(:red) + + lock_files.each { |lock_file| puts " #{lock_file}" } + else + puts "No ref lock files exist".color(:green) + end + end end end diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index 4d880c05f99..4507b841964 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -21,8 +21,8 @@ namespace :gitlab do command << 'BUNDLE_FLAGS=--no-deployment' if Rails.env.test? + Gitlab::SetupHelper.create_gitaly_configuration(args.dir) Dir.chdir(args.dir) do - create_gitaly_configuration # In CI we run scripts/gitaly-test-build instead of this command unless ENV['CI'].present? Bundler.with_original_env { run_command!(command) } @@ -39,60 +39,7 @@ namespace :gitlab do # Exclude gitaly-ruby configuration because that depends on the gitaly # installation directory. - puts gitaly_configuration_toml(gitaly_ruby: false) - end - - private - - # We cannot create config.toml files for all possible Gitaly configuations. - # For instance, if Gitaly is running on another machine then it makes no - # sense to write a config.toml file on the current machine. This method will - # only generate a configuration for the most common and simplest case: when - # we have exactly one Gitaly process and we are sure it is running locally - # because it uses a Unix socket. - # For development and testing purposes, an extra storage is added to gitaly, - # which is not known to Rails, but must be explicitly stubbed. - def gitaly_configuration_toml(gitaly_ruby: true) - storages = [] - address = nil - - Gitlab.config.repositories.storages.each do |key, val| - if address - if address != val['gitaly_address'] - raise ArgumentError, "Your gitlab.yml contains more than one gitaly_address." - end - elsif URI(val['gitaly_address']).scheme != 'unix' - raise ArgumentError, "Automatic config.toml generation only supports 'unix:' addresses." - else - address = val['gitaly_address'] - end - - storages << { name: key, path: val['path'] } - end - - if Rails.env.test? - storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s } - end - - config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages } - config[:auth] = { token: 'secret' } if Rails.env.test? - config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby - config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path } - config[:bin_dir] = Gitlab.config.gitaly.client_path - - TOML.dump(config) - end - - def create_gitaly_configuration - File.open("config.toml", File::WRONLY | File::CREAT | File::EXCL) do |f| - f.puts gitaly_configuration_toml - end - rescue Errno::EEXIST - puts "Skipping config.toml generation:" - puts "A configuration file already exists." - rescue ArgumentError => e - puts "Skipping config.toml generation:" - puts e.message + puts Gitlab::SetupHelper.gitaly_configuration_toml('', gitaly_ruby: false) end end end diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb index 6723662703c..c1182af1014 100644 --- a/lib/tasks/gitlab/task_helpers.rb +++ b/lib/tasks/gitlab/task_helpers.rb @@ -130,7 +130,7 @@ module Gitlab def all_repos Gitlab.config.repositories.storages.each_value do |repository_storage| - IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find| + IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -type d -name *.git)) do |find| find.each_line do |path| yield path.chomp end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 67b8901f8fb..882a2756b72 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -81,14 +81,14 @@ feature 'Gcp Cluster', :js do end it 'user sees a cluster details page' do - expect(page).to have_button('Save') + expect(page).to have_button('Save changes') expect(page.find(:css, '.cluster-name').value).to eq(cluster.name) end context 'when user disables the cluster' do before do page.find(:css, '.js-toggle-cluster').click - click_button 'Save' + page.within('#cluster-integration') { click_button 'Save changes' } end it 'user sees the successful message' do @@ -99,7 +99,7 @@ feature 'Gcp Cluster', :js do context 'when user changes cluster parameters' do before do fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' - click_button 'Save changes' + page.within('#js-cluster-details') { click_button 'Save changes' } end it 'user sees the successful message' do diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index 414f4acba86..a519b9f9c7e 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -29,7 +29,7 @@ feature 'User Cluster', :js do end it 'user sees a cluster details page' do - expect(page).to have_content('Enable cluster integration') + expect(page).to have_content('Cluster integration') expect(page.find_field('cluster[name]').value).to eq('dev-cluster') expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value) .to have_content('http://example.com') @@ -57,14 +57,14 @@ feature 'User Cluster', :js do end it 'user sees a cluster details page' do - expect(page).to have_button('Save') + expect(page).to have_button('Save changes') end context 'when user disables the cluster' do before do page.find(:css, '.js-toggle-cluster').click fill_in 'cluster_name', with: 'dev-cluster' - click_button 'Save' + page.within('#cluster-integration') { click_button 'Save changes' } end it 'user sees the successful message' do @@ -76,7 +76,7 @@ feature 'User Cluster', :js do before do fill_in 'cluster_name', with: 'my-dev-cluster' fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace' - click_button 'Save changes' + page.within('#js-cluster-details') { click_button 'Save changes' } end it 'user sees the successful message' do diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index d507af3fd3d..06031aee217 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -56,6 +56,16 @@ describe LabelsFinder do expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5] end + + context 'when only_group_labels is true' do + it 'returns only group labels' do + group_1.add_developer(user) + + finder = described_class.new(user, group_id: group_1.id, only_group_labels: true) + + expect(finder.execute).to eq [group_label_2, group_label_1] + end + end end context 'filtering by project_id' do diff --git a/spec/fixtures/api/schemas/public_api/v4/board.json b/spec/fixtures/api/schemas/public_api/v4/board.json new file mode 100644 index 00000000000..d667f1d631c --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/board.json @@ -0,0 +1,86 @@ +{ + "type": "object", + "required" : [ + "id", + "project", + "lists" + ], + "properties" : { + "id": { "type": "integer" }, + "project": { + "type": ["object", "null"], + "required": [ + "id", + "avatar_url", + "description", + "default_branch", + "tag_list", + "ssh_url_to_repo", + "http_url_to_repo", + "web_url", + "name", + "name_with_namespace", + "path", + "path_with_namespace", + "star_count", + "forks_count", + "created_at", + "last_activity_at" + ], + "properties": { + "id": { "type": "integer" }, + "avatar_url": { "type": ["string", "null"] }, + "description": { "type": ["string", "null"] }, + "default_branch": { "type": ["string", "null"] }, + "tag_list": { "type": "array" }, + "ssh_url_to_repo": { "type": "string" }, + "http_url_to_repo": { "type": "string" }, + "web_url": { "type": "string" }, + "name": { "type": "string" }, + "name_with_namespace": { "type": "string" }, + "path": { "type": "string" }, + "path_with_namespace": { "type": "string" }, + "star_count": { "type": "integer" }, + "forks_count": { "type": "integer" }, + "created_at": { "type": "date" }, + "last_activity_at": { "type": "date" } + }, + "additionalProperties": false + }, + "lists": { + "type": "array", + "items": { + "type": "object", + "required" : [ + "id", + "label", + "position" + ], + "properties" : { + "id": { "type": "integer" }, + "label": { + "type": ["object", "null"], + "required": [ + "id", + "color", + "description", + "name" + ], + "properties": { + "id": { "type": "integer" }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$" + }, + "description": { "type": ["string", "null"] }, + "name": { "type": "string" } + } + }, + "position": { "type": ["integer", "null"] } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": true +} diff --git a/spec/fixtures/api/schemas/public_api/v4/boards.json b/spec/fixtures/api/schemas/public_api/v4/boards.json new file mode 100644 index 00000000000..117564ef77a --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/boards.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "board.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json index 4ba6422406c..e8c17298b43 100644 --- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json @@ -3,6 +3,7 @@ "properties": { "domain": { "type": "string" }, "url": { "type": "uri" }, + "project_id": { "type": "integer" }, "certificate_expiration": { "type": "object", "properties": { @@ -13,6 +14,6 @@ "additionalProperties": false } }, - "required": ["domain", "url"], + "required": ["domain", "url", "project_id"], "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basic.json b/spec/fixtures/api/schemas/public_api/v4/user/basic.json index 9f69d31971c..bf330d8278c 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/basic.json @@ -1,5 +1,5 @@ { - "type": "object", + "type": ["object", "null"], "required": [ "id", "state", diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js index 7a5c1da4d1d..6d6fb410859 100644 --- a/spec/javascripts/groups/components/item_actions_spec.js +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -47,17 +47,11 @@ describe('ItemActionsComponent', () => { it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { spyOn(eventHub, '$emit'); vm.modalStatus = true; - vm.leaveGroup(true); - expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); - }); - it('should change `modalStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { - spyOn(eventHub, '$emit'); - vm.modalStatus = true; - vm.leaveGroup(false); + vm.leaveGroup(); + expect(vm.modalStatus).toBeFalsy(); - expect(eventHub.$emit).not.toHaveBeenCalled(); + expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); }); }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index 1f46c225071..6f8dad6b835 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -62,4 +62,14 @@ describe('text_utility', () => { expect(textUtils.slugify('João')).toEqual('joão'); }); }); + + describe('stripeHtml', () => { + it('replaces html tag with the default replacement', () => { + expect(textUtils.stripeHtml('This is a text with <p>html</p>.')).toEqual('This is a text with html.'); + }); + + it('replaces html tags with the provided replacement', () => { + expect(textUtils.stripeHtml('This is a text with <p>html</p>.', ' ')).toEqual('This is a text with html .'); + }); + }); }); diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js index 2e94948cfb2..588b61196a5 100644 --- a/spec/javascripts/profile/account/components/delete_account_modal_spec.js +++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js @@ -51,7 +51,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).toHaveClass('disabled'); + expect(submitButton).toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).not.toHaveBeenCalled(); }) @@ -68,7 +68,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredPassword).toBe(input.value); - expect(submitButton).not.toHaveClass('disabled'); + expect(submitButton).not.toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).toHaveBeenCalled(); }) @@ -101,7 +101,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).toHaveClass('disabled'); + expect(submitButton).toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).not.toHaveBeenCalled(); }) @@ -118,7 +118,7 @@ describe('DeleteAccountModal component', () => { Vue.nextTick() .then(() => { expect(vm.enteredUsername).toBe(input.value); - expect(submitButton).not.toHaveClass('disabled'); + expect(submitButton).not.toHaveAttr('disabled', 'disabled'); submitButton.click(); expect(form.submit).toHaveBeenCalled(); }) diff --git a/spec/javascripts/projects/project_new_spec.js b/spec/javascripts/projects/project_new_spec.js index 850768f0e4f..c314ca8ab72 100644 --- a/spec/javascripts/projects/project_new_spec.js +++ b/spec/javascripts/projects/project_new_spec.js @@ -6,8 +6,12 @@ describe('New Project', () => { beforeEach(() => { setFixtures(` - <input id="project_import_url" /> - <input id="project_path" /> + <div class='toggle-import-form'> + <div class='import-url-data'> + <input id="project_import_url" /> + <input id="project_path" /> + </div> + </div> `); $projectImportUrl = $('#project_import_url'); @@ -25,7 +29,7 @@ describe('New Project', () => { it('does not change project path for disabled $projectImportUrl', () => { $projectImportUrl.attr('disabled', true); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -38,7 +42,7 @@ describe('New Project', () => { it('does not change project path if it is set by user', () => { $projectPath.keyup(); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -46,7 +50,7 @@ describe('New Project', () => { it('does not change project path for empty $projectImportUrl', () => { $projectImportUrl.val(''); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -54,7 +58,7 @@ describe('New Project', () => { it('does not change project path for whitespace $projectImportUrl', () => { $projectImportUrl.val(' '); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -62,7 +66,7 @@ describe('New Project', () => { it('does not change project path for $projectImportUrl without slashes', () => { $projectImportUrl.val('has-no-slash'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual(dummyImportUrl); }); @@ -70,7 +74,7 @@ describe('New Project', () => { it('changes project path to last $projectImportUrl component', () => { $projectImportUrl.val('/this/is/last'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('last'); }); @@ -78,7 +82,7 @@ describe('New Project', () => { it('ignores trailing slashes in $projectImportUrl', () => { $projectImportUrl.val('/has/trailing/slash/'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('slash'); }); @@ -86,7 +90,7 @@ describe('New Project', () => { it('ignores fragment identifier in $projectImportUrl', () => { $projectImportUrl.val('/this/has/a#fragment-identifier/'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('a'); }); @@ -94,7 +98,7 @@ describe('New Project', () => { it('ignores query string in $projectImportUrl', () => { $projectImportUrl.val('/url/with?query=string'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('with'); }); @@ -102,7 +106,7 @@ describe('New Project', () => { it('ignores trailing .git in $projectImportUrl', () => { $projectImportUrl.val('/repository.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('repository'); }); @@ -110,7 +114,7 @@ describe('New Project', () => { it('changes project path for HTTPS URL in $projectImportUrl', () => { $projectImportUrl.val('https://username:password@gitlab.company.com/group/project.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('project'); }); @@ -118,7 +122,7 @@ describe('New Project', () => { it('changes project path for SSH URL in $projectImportUrl', () => { $projectImportUrl.val('git@gitlab.com:gitlab-org/gitlab-ce.git'); - projectNew.deriveProjectPathFromUrl($projectImportUrl, $projectPath); + projectNew.deriveProjectPathFromUrl($projectImportUrl); expect($projectPath.val()).toEqual('gitlab-ce'); }); diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js index b001c1655b4..6efbbf6d75e 100644 --- a/spec/javascripts/repo/components/new_dropdown/index_spec.js +++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js @@ -57,16 +57,17 @@ describe('new dropdown component', () => { }); }); - describe('toggleModalOpen', () => { + describe('hideModal', () => { + beforeAll((done) => { + vm.openModal = true; + Vue.nextTick(done); + }); + it('closes modal after toggling', (done) => { - vm.toggleModalOpen(); + vm.hideModal(); Vue.nextTick() .then(() => { - expect(vm.$el.querySelector('.modal')).not.toBeNull(); - }) - .then(vm.toggleModalOpen) - .then(() => { expect(vm.$el.querySelector('.modal')).toBeNull(); }) .then(done) diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js new file mode 100644 index 00000000000..a33ab689dd1 --- /dev/null +++ b/spec/javascripts/vue_shared/components/expand_button_spec.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import expandButton from '~/vue_shared/components/expand_button.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('expand button', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(expandButton); + vm = mountComponent(Component, { + slots: { + expanded: '<p>Expanded!</p>', + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a collpased button', () => { + expect(vm.$el.textContent.trim()).toEqual('...'); + }); + + it('hides expander on click', (done) => { + vm.$el.querySelector('button').click(); + vm.$nextTick(() => { + expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;'); + done(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js index 721f4044659..fe75a86cac8 100644 --- a/spec/javascripts/vue_shared/components/modal_spec.js +++ b/spec/javascripts/vue_shared/components/modal_spec.js @@ -2,11 +2,65 @@ import Vue from 'vue'; import modal from '~/vue_shared/components/modal.vue'; import mountComponent from '../../helpers/vue_mount_component_helper'; +const modalComponent = Vue.extend(modal); + describe('Modal', () => { - it('does not render a primary button if no primaryButtonLabel', () => { - const modalComponent = Vue.extend(modal); - const vm = mountComponent(modalComponent); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('props', () => { + describe('without primaryButtonLabel', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { + primaryButtonLabel: null, + }); + }); + + it('does not render a primary button', () => { + expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + }); + }); + + describe('with id', () => { + it('does not render a primary button', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { + id: 'my-modal', + }); + }); + + it('assigns the id to the modal', () => { + expect(vm.$el.querySelector('#my-modal.modal')).not.toBeNull(); + }); + + it('does not show the modal immediately', () => { + expect(vm.$el.querySelector('#my-modal.modal')).not.toHaveClass('show'); + }); + + it('does not show a backdrop', () => { + expect(vm.$el.querySelector('modal-backdrop')).toBeNull(); + }); + }); + }); + + it('works with data-toggle="modal"', (done) => { + setFixtures(` + <button id="modal-button" data-toggle="modal" data-target="#my-modal"></button> + <div id="modal-container"></div> + `); + + const modalContainer = document.getElementById('modal-container'); + const modalButton = document.getElementById('modal-button'); + vm = mountComponent(modalComponent, { + id: 'my-modal', + }, modalContainer); + const modalElement = vm.$el.querySelector('#my-modal'); + $(modalElement).on('shown.bs.modal', () => done()); - expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + modalButton.click(); + }); }); }); diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index c04a9688503..7f5946b1658 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -202,16 +202,6 @@ describe Gitlab::Git::Blob, seed_helper: true do context 'limiting' do subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) } - context 'default' do - let(:blob_size_limit) { nil } - - it 'limits to MAX_DATA_DISPLAY_SIZE' do - stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100) - - expect(subject.first.data.size).to eq(100) - end - end - context 'positive' do let(:blob_size_limit) { 10 } @@ -221,7 +211,10 @@ describe Gitlab::Git::Blob, seed_helper: true do context 'zero' do let(:blob_size_limit) { 0 } - it { expect(subject.first.data).to eq('') } + it 'only loads the metadata' do + expect(subject.first.size).not_to be(0) + expect(subject.first.data).to eq('') + end end context 'negative' do diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb index d9ddb4326be..6132abd9b35 100644 --- a/spec/lib/gitlab/ldap/adapter_spec.rb +++ b/spec/lib/gitlab/ldap/adapter_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::LDAP::Adapter do expect(adapter).to receive(:ldap_search) do |arg| expect(arg[:filter].to_s).to eq('(uid=johndoe)') expect(arg[:base]).to eq('dc=example,dc=com') - expect(arg[:attributes]).to match(%w{dn uid cn mail email userPrincipalName}) + expect(arg[:attributes]).to match(ldap_attributes) end.and_return({}) adapter.users('uid', 'johndoe') @@ -26,7 +26,7 @@ describe Gitlab::LDAP::Adapter do expect(adapter).to receive(:ldap_search).with( base: 'uid=johndoe,ou=users,dc=example,dc=com', scope: Net::LDAP::SearchScope_BaseObject, - attributes: %w{dn uid cn mail email userPrincipalName}, + attributes: ldap_attributes, filter: nil ).and_return({}) @@ -63,7 +63,7 @@ describe Gitlab::LDAP::Adapter do it 'uses the right uid attribute when non-default' do stub_ldap_config(uid: 'sAMAccountName') expect(adapter).to receive(:ldap_search).with( - hash_including(attributes: %w{dn sAMAccountName cn mail email userPrincipalName}) + hash_including(attributes: ldap_attributes) ).and_return({}) adapter.users('sAMAccountName', 'johndoe') @@ -137,4 +137,8 @@ describe Gitlab::LDAP::Adapter do end end end + + def ldap_attributes + Gitlab::LDAP::Person.ldap_attributes(Gitlab::LDAP::Config.new('ldapmain')) + end end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index d204050ef66..ff29d9aa5be 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -8,13 +8,16 @@ describe Gitlab::LDAP::Person do before do stub_ldap_config( options: { + 'uid' => 'uid', 'attributes' => { - 'name' => 'cn', - 'email' => %w(mail email userPrincipalName) + 'name' => 'cn', + 'email' => %w(mail email userPrincipalName), + 'username' => username_attribute } } ) end + let(:username_attribute) { %w(uid sAMAccountName userid) } describe '.normalize_dn' do subject { described_class.normalize_dn(given) } @@ -44,6 +47,34 @@ describe Gitlab::LDAP::Person do end end + describe '.ldap_attributes' do + it 'returns a compact and unique array' do + stub_ldap_config( + options: { + 'uid' => nil, + 'attributes' => { + 'name' => 'cn', + 'email' => 'mail', + 'username' => %w(uid mail memberof) + } + } + ) + config = Gitlab::LDAP::Config.new('ldapmain') + ldap_attributes = described_class.ldap_attributes(config) + + expect(ldap_attributes).to match_array(%w(dn uid cn mail memberof)) + end + end + + describe '.validate_entry' do + it 'raises InvalidEntryError' do + entry['foo'] = 'bar' + + expect { described_class.new(entry, 'ldapmain') } + .to raise_error(Gitlab::LDAP::Person::InvalidEntryError) + end + end + describe '#name' do it 'uses the configured name attribute and handles values as an array' do name = 'John Doe' @@ -72,6 +103,44 @@ describe Gitlab::LDAP::Person do end end + describe '#username' do + context 'with default uid username attribute' do + let(:username_attribute) { 'uid' } + + it 'returns the proper username value' do + attr_value = 'johndoe' + entry[username_attribute] = attr_value + person = described_class.new(entry, 'ldapmain') + + expect(person.username).to eq(attr_value) + end + end + + context 'with a different username attribute' do + let(:username_attribute) { 'sAMAccountName' } + + it 'returns the proper username value' do + attr_value = 'johndoe' + entry[username_attribute] = attr_value + person = described_class.new(entry, 'ldapmain') + + expect(person.username).to eq(attr_value) + end + end + + context 'with a non-standard username attribute' do + let(:username_attribute) { 'mail' } + + it 'returns the proper username value' do + attr_value = 'john.doe@example.com' + entry[username_attribute] = attr_value + person = described_class.new(entry, 'ldapmain') + + expect(person.username).to eq(attr_value) + end + end + end + def assert_generic_test(test_description, got, expected) test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}" expect(got).to eq(expected), test_failure_message diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 6334bcd0156..45fff4c5787 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -275,6 +275,26 @@ describe Gitlab::OAuth::User do end end + context 'and a corresponding LDAP person with a non-default username' do + before do + allow(ldap_user).to receive(:uid) { uid } + allow(ldap_user).to receive(:username) { 'johndoe@example.com' } + allow(ldap_user).to receive(:email) { %w(johndoe@example.com john2@example.com) } + allow(ldap_user).to receive(:dn) { dn } + end + + context 'and no account for the LDAP user' do + it 'creates a user favoring the LDAP username and strips email domain' do + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + + oauth_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'johndoe' + end + end + end + context "and no corresponding LDAP person" do before do allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 0678cae9b93..b3f160f3119 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -250,9 +250,13 @@ describe Namespace do parent.update(path: 'mygroup_new') - expect(project_in_parent_group.repo.config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}" - expect(hashed_project_in_subgroup.repo.config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}" - expect(legacy_project_in_subgroup.repo.config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{legacy_project_in_subgroup.path}" + expect(project_rugged(project_in_parent_group).config['gitlab.fullpath']).to eq "mygroup_new/#{project_in_parent_group.path}" + expect(project_rugged(hashed_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{hashed_project_in_subgroup.path}" + expect(project_rugged(legacy_project_in_subgroup).config['gitlab.fullpath']).to eq "mygroup_new/mysubgroup/#{legacy_project_in_subgroup.path}" + end + + def project_rugged(project) + project.repository.rugged end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 13e5345ee4c..32f40f8c365 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2639,7 +2639,7 @@ describe Project do project.rename_repo - expect(project.repo.config['gitlab.fullpath']).to eq(project.full_path) + expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path) end end @@ -2800,7 +2800,7 @@ describe Project do it 'updates project full path in .git/config' do project.rename_repo - expect(project.repo.config['gitlab.fullpath']).to eq(project.full_path) + expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path) end end @@ -3150,13 +3150,13 @@ describe Project do it 'writes full path in .git/config when key is missing' do project.write_repository_config - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end it 'updates full path in .git/config when key is present' do project.write_repository_config(gl_full_path: 'old/path') - expect { project.write_repository_config }.to change { project.repo.config['gitlab.fullpath'] }.from('old/path').to(project.full_path) + expect { project.write_repository_config }.to change { project.repository.rugged.config['gitlab.fullpath'] }.from('old/path').to(project.full_path) end it 'does not raise an error with an empty repository' do diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index f65af69dc7f..c6c10025f7f 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -6,18 +6,18 @@ describe API::Boards do set(:non_member) { create(:user) } set(:guest) { create(:user) } set(:admin) { create(:user, :admin) } - set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } + set(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) } set(:dev_label) do - create(:label, title: 'Development', color: '#FFAABB', project: project) + create(:label, title: 'Development', color: '#FFAABB', project: board_parent) end set(:test_label) do - create(:label, title: 'Testing', color: '#FFAACC', project: project) + create(:label, title: 'Testing', color: '#FFAACC', project: board_parent) end set(:ux_label) do - create(:label, title: 'UX', color: '#FF0000', project: project) + create(:label, title: 'UX', color: '#FF0000', project: board_parent) end set(:dev_list) do @@ -28,180 +28,25 @@ describe API::Boards do create(:list, label: test_label, position: 2) end - set(:board) do - create(:board, project: project, lists: [dev_list, test_list]) - end - - before do - project.add_reporter(user) - project.add_guest(guest) - end + set(:milestone) { create(:milestone, project: board_parent) } + set(:board_label) { create(:label, project: board_parent) } + set(:board) { create(:board, project: board_parent, lists: [dev_list, test_list]) } - describe "GET /projects/:id/boards" do - let(:base_url) { "/projects/#{project.id}/boards" } + it_behaves_like 'group and project boards', "/projects/:id/boards" - context "when unauthenticated" do - it "returns authentication error" do - get api(base_url) - - expect(response).to have_gitlab_http_status(401) - end - end - - context "when authenticated" do - it "returns the project issue board" do - get api(base_url, user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(1) - expect(json_response.first['id']).to eq(board.id) - expect(json_response.first['lists']).to be_an Array - expect(json_response.first['lists'].length).to eq(2) - expect(json_response.first['lists'].last).to have_key('position') - end - end - end - - describe "GET /projects/:id/boards/:board_id/lists" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it 'returns issue board lists' do - get api(base_url, user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['label']['name']).to eq(dev_label.title) - end - - it 'returns 404 if board not found' do - get api("/projects/#{project.id}/boards/22343/lists", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "GET /projects/:id/boards/:board_id/lists/:list_id" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it 'returns a list' do - get api("#{base_url}/#{dev_list.id}", user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['id']).to eq(dev_list.id) - expect(json_response['label']['name']).to eq(dev_label.title) - expect(json_response['position']).to eq(1) - end - - it 'returns 404 if list not found' do - get api("#{base_url}/5324", user) - - expect(response).to have_gitlab_http_status(404) - end - end - - describe "POST /projects/:id/board/lists" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } + describe "POST /projects/:id/boards/lists" do + let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}/lists" } it 'creates a new issue board list for group labels' do group = create(:group) group_label = create(:group_label, group: group) - project.update(group: group) + board_parent.update(group: group) - post api(base_url, user), label_id: group_label.id + post api(url, user), label_id: group_label.id expect(response).to have_gitlab_http_status(201) expect(json_response['label']['name']).to eq(group_label.title) expect(json_response['position']).to eq(3) end - - it 'creates a new issue board list for project labels' do - post api(base_url, user), label_id: ux_label.id - - expect(response).to have_gitlab_http_status(201) - expect(json_response['label']['name']).to eq(ux_label.title) - expect(json_response['position']).to eq(3) - end - - it 'returns 400 when creating a new list if label_id is invalid' do - post api(base_url, user), label_id: 23423 - - expect(response).to have_gitlab_http_status(400) - end - - it 'returns 403 for project members with guest role' do - put api("#{base_url}/#{test_list.id}", guest), position: 1 - - expect(response).to have_gitlab_http_status(403) - end - end - - describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it "updates a list" do - put api("#{base_url}/#{test_list.id}", user), - position: 1 - - expect(response).to have_gitlab_http_status(200) - expect(json_response['position']).to eq(1) - end - - it "returns 404 error if list id not found" do - put api("#{base_url}/44444", user), - position: 1 - - expect(response).to have_gitlab_http_status(404) - end - - it "returns 403 for project members with guest role" do - put api("#{base_url}/#{test_list.id}", guest), - position: 1 - - expect(response).to have_gitlab_http_status(403) - end - end - - describe "DELETE /projects/:id/board/lists/:list_id" do - let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" } - - it "rejects a non member from deleting a list" do - delete api("#{base_url}/#{dev_list.id}", non_member) - - expect(response).to have_gitlab_http_status(403) - end - - it "rejects a user with guest role from deleting a list" do - delete api("#{base_url}/#{dev_list.id}", guest) - - expect(response).to have_gitlab_http_status(403) - end - - it "returns 404 error if list id not found" do - delete api("#{base_url}/44444", user) - - expect(response).to have_gitlab_http_status(404) - end - - context "when the user is project owner" do - set(:owner) { create(:user) } - - before do - project.update(namespace: owner.namespace) - end - - it "deletes the list if an admin requests it" do - delete api("#{base_url}/#{dev_list.id}", owner) - - expect(response).to have_gitlab_http_status(204) - end - - it_behaves_like '412 response' do - let(:request) { api("#{base_url}/#{dev_list.id}", owner) } - end - end end end diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index d412b045e9f..5d01dc37f0e 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -46,6 +46,7 @@ describe API::PagesDomains do expect(json_response).to be_an Array expect(json_response.size).to eq(3) expect(json_response.last).to have_key('domain') + expect(json_response.last).to have_key('project_id') expect(json_response.last).to have_key('certificate_expiration') expect(json_response.last['certificate_expiration']['expired']).to be true expect(json_response.first).not_to have_key('certificate_expiration') diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 1833078f37c..9a44dfde41b 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -255,7 +255,7 @@ describe Projects::CreateService, '#execute' do it 'writes project full path to .git/config' do project = create_project(user, opts) - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end def create_project(user, opts) diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb index ded864beb1d..7b536cc05cb 100644 --- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -37,7 +37,7 @@ describe Projects::HashedStorage::MigrateRepositoryService do it 'writes project full path to .git/config' do service.execute - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 7377c748698..39f6388c25e 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -58,7 +58,7 @@ describe Projects::TransferService do it 'updates project full path in .git/config' do transfer_project(project, user, group) - expect(project.repo.config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" + expect(project.repository.rugged.config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" end end @@ -95,7 +95,7 @@ describe Projects::TransferService do it 'rolls back project full path in .git/config' do attempt_project_transfer - expect(project.repo.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path end it "doesn't send move notifications" do diff --git a/spec/support/api/boards_shared_examples.rb b/spec/support/api/boards_shared_examples.rb new file mode 100644 index 00000000000..943c1f6ffd7 --- /dev/null +++ b/spec/support/api/boards_shared_examples.rb @@ -0,0 +1,180 @@ +shared_examples_for 'group and project boards' do |route_definition, ee = false| + let(:root_url) { route_definition.gsub(":id", board_parent.id.to_s) } + + before do + board_parent.add_reporter(user) + board_parent.add_guest(guest) + end + + def expect_schema_match_for(response, schema_file, ee) + if ee + expect(response).to match_response_schema(schema_file, dir: "ee") + else + expect(response).to match_response_schema(schema_file) + end + end + + describe "GET #{route_definition}" do + context "when unauthenticated" do + it "returns authentication error" do + get api(root_url) + + expect(response).to have_gitlab_http_status(401) + end + end + + context "when authenticated" do + it "returns the issue boards" do + get api(root_url, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect_schema_match_for(response, 'public_api/v4/boards', ee) + end + + describe "GET #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'get a single board by id' do + get api(url, user) + + expect_schema_match_for(response, 'public_api/v4/board', ee) + end + end + end + end + + describe "GET #{route_definition}/:board_id/lists" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'returns issue board lists' do + get api(url, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['label']['name']).to eq(dev_label.title) + end + + it 'returns 404 if board not found' do + get api("#{root_url}/22343/lists", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe "GET #{route_definition}/:board_id/lists/:list_id" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'returns a list' do + get api("#{url}/#{dev_list.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['id']).to eq(dev_list.id) + expect(json_response['label']['name']).to eq(dev_label.title) + expect(json_response['position']).to eq(1) + end + + it 'returns 404 if list not found' do + get api("#{url}/5324", user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe "POST #{route_definition}/lists" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it 'creates a new issue board list for labels' do + post api(url, user), label_id: ux_label.id + + expect(response).to have_gitlab_http_status(201) + expect(json_response['label']['name']).to eq(ux_label.title) + expect(json_response['position']).to eq(3) + end + + it 'returns 400 when creating a new list if label_id is invalid' do + post api(url, user), label_id: 23423 + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 403 for members with guest role' do + put api("#{url}/#{test_list.id}", guest), position: 1 + + expect(response).to have_gitlab_http_status(403) + end + end + + describe "PUT #{route_definition}/:board_id/lists/:list_id to update only position" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it "updates a list" do + put api("#{url}/#{test_list.id}", user), + position: 1 + + expect(response).to have_gitlab_http_status(200) + expect(json_response['position']).to eq(1) + end + + it "returns 404 error if list id not found" do + put api("#{url}/44444", user), + position: 1 + + expect(response).to have_gitlab_http_status(404) + end + + it "returns 403 for members with guest role" do + put api("#{url}/#{test_list.id}", guest), + position: 1 + + expect(response).to have_gitlab_http_status(403) + end + end + + describe "DELETE #{route_definition}/lists/:list_id" do + let(:url) { "#{root_url}/#{board.id}/lists" } + + it "rejects a non member from deleting a list" do + delete api("#{url}/#{dev_list.id}", non_member) + + expect(response).to have_gitlab_http_status(403) + end + + it "rejects a user with guest role from deleting a list" do + delete api("#{url}/#{dev_list.id}", guest) + + expect(response).to have_gitlab_http_status(403) + end + + it "returns 404 error if list id not found" do + delete api("#{url}/44444", user) + + expect(response).to have_gitlab_http_status(404) + end + + context "when the user is parent owner" do + set(:owner) { create(:user) } + + before do + if board_parent.try(:namespace) + board_parent.update(namespace: owner.namespace) + else + board.parent.add_owner(owner) + end + end + + it "deletes the list if an admin requests it" do + delete api("#{url}/#{dev_list.id}", owner) + + expect(response).to have_gitlab_http_status(204) + end + + it_behaves_like '412 response' do + let(:request) { api("#{url}/#{dev_list.id}", owner) } + end + end + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 1d99746b09f..664698fcbaf 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -1,4 +1,5 @@ require 'rspec/mocks' +require 'toml' module TestEnv extend self @@ -147,6 +148,9 @@ module TestEnv version: Gitlab::GitalyClient.expected_server_version, task: "gitlab:gitaly:install[#{gitaly_dir}]") do + # Always re-create config, in case it's outdated. This is fast anyway. + Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, force: true) + start_gitaly(gitaly_dir) end end @@ -347,6 +351,9 @@ module TestEnv end def component_needs_update?(component_folder, expected_version) + # Allow local overrides of the component for tests during development + return false if Rails.env.test? && File.symlink?(component_folder) + version = File.read(File.join(component_folder, 'VERSION')).strip # Notice that this will always yield true when using branch versions diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb new file mode 100644 index 00000000000..dacc5dc5ae7 --- /dev/null +++ b/spec/tasks/gitlab/git_rake_spec.rb @@ -0,0 +1,38 @@ +require 'rake_helper' + +describe 'gitlab:git rake tasks' do + before do + Rake.application.rake_require 'tasks/gitlab/git' + + storages = { 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') } } + + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git')) + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow_any_instance_of(String).to receive(:color) { |string, _color| string } + + stub_warn_user_is_not_gitlab + end + + after do + FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage')) + end + + describe 'fsck' do + it 'outputs the integrity check for a repo' do + expect { run_rake_task('gitlab:git:fsck') }.to output(/Performed Checking integrity at .*@hashed\/1\/2\/test.git/).to_stdout + end + + it 'errors out about config.lock issues' do + FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/config.lock')) + + expect { run_rake_task('gitlab:git:fsck') }.to output(/file exists\? ... yes/).to_stdout + end + + it 'errors out about ref lock issues' do + FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/refs/heads')) + FileUtils.touch(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git/refs/heads/blah.lock')) + + expect { run_rake_task('gitlab:git:fsck') }.to output(/Ref lock files exist:/).to_stdout + end + end +end |