diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-14 15:09:08 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-14 15:09:08 +0000 |
commit | b3a736ed88a1db0391cd9881e70b987bab7d89d1 (patch) | |
tree | a91ca3a06abd4c3412775ac3c49b11e3151df2be | |
parent | 5366964a10484c2783a646b35a6da9eece01b242 (diff) | |
download | gitlab-ce-b3a736ed88a1db0391cd9881e70b987bab7d89d1.tar.gz |
Add latest changes from gitlab-org/gitlab@master
74 files changed, 1429 insertions, 324 deletions
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index d9cd4f3acf1..2581c3e9928 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -70,7 +70,7 @@ export default { :title="$options.currentBranchPermissionsTooltip" > <span - class="ide-radio-label" + class="ide-option-label" data-qa-selector="commit_to_current_branch_radio" v-html="commitToCurrentBranchText" ></span> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue index daa44a42765..0812599c25c 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue @@ -1,16 +1,27 @@ <script> import { createNamespacedHelpers } from 'vuex'; +import { GlTooltipDirective } from '@gitlab/ui'; +import { s__ } from '~/locale'; -const { - mapState: mapCommitState, - mapActions: mapCommitActions, - mapGetters: mapCommitGetters, -} = createNamespacedHelpers('commit'); +const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNamespacedHelpers( + 'commit', +); export default { + directives: { + GlTooltip: GlTooltipDirective, + }, computed: { - ...mapCommitState(['shouldCreateMR']), - ...mapCommitGetters(['shouldHideNewMrOption']), + ...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']), + tooltipText() { + if (this.shouldDisableNewMrOption) { + return s__( + 'IDE|This option is disabled because you are not allowed to create merge requests in this project.', + ); + } + + return ''; + }, }, methods: { ...mapCommitActions(['toggleShouldCreateMR']), @@ -21,14 +32,19 @@ export default { <template> <fieldset v-if="!shouldHideNewMrOption"> <hr class="my-2" /> - <label class="mb-0 js-ide-commit-new-mr"> + <label + v-gl-tooltip="tooltipText" + class="mb-0 js-ide-commit-new-mr" + :class="{ 'is-disabled': shouldDisableNewMrOption }" + > <input + :disabled="shouldDisableNewMrOption" :checked="shouldCreateMR" type="checkbox" data-qa-selector="start_new_mr_checkbox" @change="toggleShouldCreateMR" /> - <span class="prepend-left-10"> + <span class="prepend-left-10 ide-option-label"> {{ __('Start a new merge request') }} </span> </label> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 07073f5f879..a9591805261 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -67,7 +67,7 @@ export default { @change="updateCommitAction($event.target.value)" /> <span class="prepend-left-10"> - <span v-if="label" class="ide-radio-label"> {{ label }} </span> <slot v-else></slot> + <span v-if="label" class="ide-option-label"> {{ label }} </span> <slot v-else></slot> </span> </label> <div v-if="commitAction === value && showInput" class="ide-commit-new-branch"> diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue index 2e290de0943..2307efd1d24 100644 --- a/app/assets/javascripts/ide/components/nav_dropdown.vue +++ b/app/assets/javascripts/ide/components/nav_dropdown.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; +import { mapGetters } from 'vuex'; import NavForm from './nav_form.vue'; import NavDropdownButton from './nav_dropdown_button.vue'; @@ -13,6 +14,9 @@ export default { isVisibleDropdown: false, }; }, + computed: { + ...mapGetters(['canReadMergeRequests']), + }, mounted() { this.addDropdownListeners(); }, @@ -42,7 +46,9 @@ export default { <template> <div ref="dropdown" class="btn-group ide-nav-dropdown dropdown"> - <nav-dropdown-button /> - <div class="dropdown-menu dropdown-menu-left p-0"><nav-form v-if="isVisibleDropdown" /></div> + <nav-dropdown-button :show-merge-requests="canReadMergeRequests" /> + <div class="dropdown-menu dropdown-menu-left p-0"> + <nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" /> + </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue index f1d44443125..4cd320d5d66 100644 --- a/app/assets/javascripts/ide/components/nav_dropdown_button.vue +++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue @@ -10,6 +10,13 @@ export default { Icon, DropdownButton, }, + props: { + showMergeRequests: { + type: Boolean, + required: false, + default: true, + }, + }, computed: { ...mapState(['currentBranchId', 'currentMergeRequestId']), mergeRequestLabel() { @@ -25,10 +32,10 @@ export default { <template> <dropdown-button> <span class="row"> - <span class="col-7 text-truncate"> + <span class="col-auto text-truncate" :class="{ 'col-7': showMergeRequests }"> <icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }} </span> - <span class="col-5 pl-0 text-truncate"> + <span v-if="showMergeRequests" class="col-5 pl-0 text-truncate"> <icon :size="16" :aria-label="__('Merge Request')" name="merge-request" /> {{ mergeRequestLabel }} </span> diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue index 2ccc84ea5d5..195504a6861 100644 --- a/app/assets/javascripts/ide/components/nav_form.vue +++ b/app/assets/javascripts/ide/components/nav_form.vue @@ -11,12 +11,19 @@ export default { BranchesSearchList, MergeRequestSearchList, }, + props: { + showMergeRequests: { + type: Boolean, + required: false, + default: true, + }, + }, }; </script> <template> <div class="ide-nav-form p-0"> - <tabs stop-propagation> + <tabs v-if="showMergeRequests" stop-propagation> <tab active> <template slot="title"> {{ __('Branches') }} @@ -30,5 +37,6 @@ export default { <merge-request-search-list /> </tab> </tabs> + <branches-search-list v-else /> </div> </template> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 673ac1bfa9a..54d3e79411f 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -8,6 +8,9 @@ export const MAX_BODY_LENGTH = 72; export const FILE_VIEW_MODE_EDITOR = 'editor'; export const FILE_VIEW_MODE_PREVIEW = 'preview'; +export const PERMISSION_CREATE_MR = 'createMergeRequestIn'; +export const PERMISSION_READ_MR = 'readMergeRequest'; + export const activityBarViews = { edit: 'ide-tree', commit: 'commit-section', diff --git a/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql b/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql new file mode 100644 index 00000000000..48f63995f44 --- /dev/null +++ b/app/assets/javascripts/ide/queries/getUserPermissions.query.graphql @@ -0,0 +1,8 @@ +query getUserPermissions($projectPath: ID!) { + project(fullPath: $projectPath) { + userPermissions { + createMergeRequestIn, + readMergeRequest + } + } +} diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js new file mode 100644 index 00000000000..8a7f27328ba --- /dev/null +++ b/app/assets/javascripts/ide/services/gql.js @@ -0,0 +1,8 @@ +import createGqClient, { fetchPolicies } from '~/lib/graphql'; + +export default createGqClient( + {}, + { + fetchPolicy: fetchPolicies.NO_CACHE, + }, +); diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index a5134c64705..84a2b2bd58e 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -1,6 +1,18 @@ import axios from '~/lib/utils/axios_utils'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; import Api from '~/api'; +import getUserPermissions from '../queries/getUserPermissions.query.graphql'; +import gqClient from './gql'; + +const fetchApiProjectData = projectPath => Api.project(projectPath).then(({ data }) => data); + +const fetchGqlProjectData = projectPath => + gqClient + .query({ + query: getUserPermissions, + variables: { projectPath }, + }) + .then(({ data }) => data.project); export default { getFileData(endpoint) { @@ -47,7 +59,16 @@ export default { .then(({ data }) => data); }, getProjectData(namespace, project) { - return Api.project(`${namespace}/${project}`); + const projectPath = `${namespace}/${project}`; + + return Promise.all([fetchApiProjectData(projectPath), fetchGqlProjectData(projectPath)]).then( + ([apiProjectData, gqlProjectData]) => ({ + data: { + ...apiProjectData, + ...gqlProjectData, + }, + }), + ); }, getProjectMergeRequests(projectId, params = {}) { return Api.projectMergeRequests(projectId, params); diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index aa44067edf8..9e9c6fc42b3 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -2,10 +2,17 @@ import flash from '~/flash'; import { __ } from '~/locale'; import service from '../../services'; import * as types from '../mutation_types'; -import { activityBarViews } from '../../constants'; +import { activityBarViews, PERMISSION_READ_MR } from '../../constants'; -export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branchId } = {}) => - service +export const getMergeRequestsForBranch = ( + { commit, state, getters }, + { projectId, branchId } = {}, +) => { + if (!getters.findProjectPermissions(projectId)[PERMISSION_READ_MR]) { + return Promise.resolve(); + } + + return service .getProjectMergeRequests(`${projectId}`, { source_branch: branchId, source_project_id: state.projects[projectId].id, @@ -36,6 +43,7 @@ export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branch ); throw e; }); +}; export const getMergeRequestData = ( { commit, dispatch, state }, diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 2fc574cd343..257062d118c 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -1,5 +1,10 @@ import { getChangesCountForFiles, filePathMatches } from './utils'; -import { activityBarViews, packageJsonPath } from '../constants'; +import { + activityBarViews, + packageJsonPath, + PERMISSION_READ_MR, + PERMISSION_CREATE_MR, +} from '../constants'; export const activeFile = state => state.openFiles.find(file => file.active) || null; @@ -141,5 +146,14 @@ export const getDiffInfo = (state, getters) => path => { }; }; +export const findProjectPermissions = (state, getters) => projectId => + getters.findProject(projectId)?.userPermissions || {}; + +export const canReadMergeRequests = (state, getters) => + Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_READ_MR]); + +export const canCreateMergeRequests = (state, getters) => + Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_CREATE_MR]); + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 0740e0523a9..3be350db3da 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -158,7 +158,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); }, 5000); - if (state.shouldCreateMR) { + if (getters.shouldCreateMR) { const { currentProject } = rootGetters; const targetBranch = getters.isCreatingNewBranch ? rootState.currentBranchId diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index de289e27199..e421d44b6de 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -54,5 +54,11 @@ export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) (!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) && rootGetters.canPushToBranch; +export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) => + !rootGetters.canCreateMergeRequests; + +export const shouldCreateMR = (state, getters) => + state.shouldCreateMR && !getters.shouldDisableNewMrOption; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js index 089dedd14cb..78b7e29ae53 100644 --- a/app/assets/javascripts/pages/admin/application_settings/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/index.js @@ -3,9 +3,7 @@ import projectSelect from '~/project_select'; import selfMonitor from '~/self_monitor'; document.addEventListener('DOMContentLoaded', () => { - if (gon.features && gon.features.selfMonitoringProject) { - selfMonitor(); - } + selfMonitor(); // Initialize expandable settings panels initSettingsPanels(); projectSelect(); diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 29a3340b83d..2ba170998e8 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -71,7 +71,12 @@ export default { <template> <div class="tree-content-holder"> <div class="table-holder bordered-box"> - <table :aria-label="tableCaption" class="table tree-table qa-file-tree" aria-live="polite"> + <table + :aria-label="tableCaption" + class="table tree-table" + aria-live="polite" + data-qa-selector="file_tree_table" + > <table-header v-once /> <tbody> <parent-row diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 8703796b116..c905c39bbba 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -139,7 +139,13 @@ export default { class="d-inline-block align-text-bottom fa-fw" /> <i v-else :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i> - <component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated"> + <component + :is="linkComponent" + :to="routerLinkTo" + :href="url" + class="str-truncated" + data-qa-selector="file_name_link" + > {{ fullPath }} </component> <!-- eslint-disable-next-line @gitlab/vue-i18n/no-bare-strings --> diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 58279bba4ca..990aca5f0c5 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -688,7 +688,7 @@ $ide-commit-header-height: 48px; font-weight: normal; &.is-disabled { - .ide-radio-label { + .ide-option-label { text-decoration: line-through; } } diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 00771aff26c..8a583e16c0b 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -11,16 +11,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController before_action :set_application_setting before_action :whitelist_query_limiting, only: [:usage_data] - before_action :validate_self_monitoring_feature_flag_enabled, only: [ - :create_self_monitoring_project, - :status_create_self_monitoring_project, - :delete_self_monitoring_project, - :status_delete_self_monitoring_project - ] - - before_action do - push_frontend_feature_flag(:self_monitoring_project) - end VALID_SETTING_PANELS = %w(general integrations repository ci_cd reporting metrics_and_profiling @@ -163,10 +153,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController private - def validate_self_monitoring_feature_flag_enabled - self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project) - end - def self_monitoring_data { project_id: @application_setting.self_monitoring_project_id, @@ -174,16 +160,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController } end - def self_monitoring_project_not_implemented - render( - status: :not_implemented, - json: { - message: _('Self-monitoring is not enabled on this GitLab server, contact your administrator.'), - documentation_url: help_page_path('administration/monitoring/gitlab_self_monitoring_project/index') - } - ) - end - def set_application_setting @application_setting = ApplicationSetting.current_without_cache end diff --git a/app/services/projects/lsif_data_service.rb b/app/services/projects/lsif_data_service.rb index 1282a0736e7..971885b680e 100644 --- a/app/services/projects/lsif_data_service.rb +++ b/app/services/projects/lsif_data_service.rb @@ -3,7 +3,7 @@ module Projects class LsifDataService attr_reader :file, :project, :path, :commit_id, - :docs, :doc_ranges, :ranges, :def_refs + :docs, :doc_ranges, :ranges, :def_refs, :hover_refs CACHE_EXPIRE_IN = 1.hour @@ -26,7 +26,8 @@ module Projects end_line: line_data.last, start_char: column_data.first, end_char: column_data.last, - definition_url: definition_url_for(def_refs[ref_id]) + definition_url: definition_url_for(def_refs[ref_id]), + hover: highlighted_hover(hover_refs[ref_id]) } end end @@ -54,6 +55,7 @@ module Projects @doc_ranges = data['doc_ranges'] @ranges = data['ranges'] @def_refs = data['def_refs'] + @hover_refs = data['hover_refs'] end def doc_id @@ -86,5 +88,16 @@ module Projects Gitlab::Routing.url_helpers.project_blob_path(project, definition_ref_path, anchor: line_anchor) end + + def highlighted_hover(hovers) + hovers&.map do |hover| + # Documentation for a method which is added as comments on top of the method + # is stored as a raw string value in LSIF file + next { value: hover } unless hover.is_a?(Hash) + + value = Gitlab::Highlight.highlight(nil, hover['value'], language: hover['language']) + { language: hover['language'], value: value } + end + end end end diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml index ff40d7da892..0b747082de0 100644 --- a/app/views/admin/application_settings/metrics_and_profiling.html.haml +++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml @@ -47,8 +47,7 @@ .settings-content = render 'performance_bar' -- if Feature.enabled?(:self_monitoring_project) - .js-self-monitoring-settings{ data: self_monitoring_project_data } +.js-self-monitoring-settings{ data: self_monitoring_project_data } %section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) } .settings-header#usage-statistics diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 7796db5ba63..d9887cb470a 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -10,7 +10,7 @@ = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64) .d-flex.flex-column.flex-wrap.align-items-baseline .d-inline-flex.align-items-baseline - %h1.home-panel-title.prepend-top-8.append-bottom-5.qa-project-name + %h1.home-panel-title.prepend-top-8.append-bottom-5{ data: { qa_selector: 'project_name_content' } } = @project.name %span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'}) @@ -70,7 +70,7 @@ - source = visible_fork_source(@project) - if source #{ s_('ForkedFromProjectPath|Forked from') } - = link_to source.full_name, project_path(source) + = link_to source.full_name, project_path(source), data: { qa_selector: 'forked_from_link' } - else = s_('ForkedFromProjectPath|Forked from an inaccessible project') diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index cb459b031fc..c65420d537b 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -1,6 +1,6 @@ .tree-content-holder.js-tree-content{ data: tree_content_data(@logs_path, @project, @path) } .table-holder.bordered-box - %table.table#tree-slider{ class: "table_#{@hex_path} tree-table qa-file-tree" } + %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" } %thead %tr %th= s_('ProjectFileTree|Name') diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 849d9d7e87c..4d3c24aee6b 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -84,17 +84,16 @@ = render 'projects/find_file_link' - - if can_create_mr_from_fork - - if can_collaborate || current_user&.already_forked?(@project) - - if vue_file_list_enabled? - #js-tree-web-ide-link.d-inline-block - - else - = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do - = _('Web IDE') + - if can_collaborate || current_user&.already_forked?(@project) + - if vue_file_list_enabled? + #js-tree-web-ide-link.d-inline-block - else - = link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do + = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do = _('Web IDE') - = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path) + - elsif can_create_mr_from_fork + = link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do + = _('Web IDE') + = render 'shared/confirm_fork_modal', fork_path: ide_fork_and_edit_path(@project, @ref, @path) - if show_xcode_link?(@project) .project-action-button.project-xcode.inline< diff --git a/changelogs/unreleased/35428-web-ide-button-missing-from-project-pages.yml b/changelogs/unreleased/35428-web-ide-button-missing-from-project-pages.yml new file mode 100644 index 00000000000..c4b6f43cb48 --- /dev/null +++ b/changelogs/unreleased/35428-web-ide-button-missing-from-project-pages.yml @@ -0,0 +1,5 @@ +--- +title: Enable Web IDE on projects without Merge Requests +merge_request: 24508 +author: +type: fixed diff --git a/changelogs/unreleased/dblessing_disable_enforced_sso_plan_expires.yml b/changelogs/unreleased/dblessing_disable_enforced_sso_plan_expires.yml new file mode 100644 index 00000000000..5e202039513 --- /dev/null +++ b/changelogs/unreleased/dblessing_disable_enforced_sso_plan_expires.yml @@ -0,0 +1,5 @@ +--- +title: When a namespace GitLab Subscription expires, disable SSO enforcement +merge_request: 21135 +author: +type: fixed diff --git a/changelogs/unreleased/georgekoltsov-fix-group-import-visibility-level.yml b/changelogs/unreleased/georgekoltsov-fix-group-import-visibility-level.yml new file mode 100644 index 00000000000..61ec0dc6fc7 --- /dev/null +++ b/changelogs/unreleased/georgekoltsov-fix-group-import-visibility-level.yml @@ -0,0 +1,6 @@ +--- +title: Use closest allowed visibility level on group creation when importing groups + using Group Import/Export +merge_request: 25026 +author: +type: fixed diff --git a/changelogs/unreleased/refactoring-entities-file-27.yml b/changelogs/unreleased/refactoring-entities-file-27.yml new file mode 100644 index 00000000000..e4b76bfa8dc --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-27.yml @@ -0,0 +1,5 @@ +--- +title: Separate entities into own class files +merge_request: 24985 +author: Rajendra Kadam +type: added diff --git a/changelogs/unreleased/refactoring-entities-file-29.yml b/changelogs/unreleased/refactoring-entities-file-29.yml new file mode 100644 index 00000000000..633d7069406 --- /dev/null +++ b/changelogs/unreleased/refactoring-entities-file-29.yml @@ -0,0 +1,5 @@ +--- +title: Separate Application and Blob entities into own class files +merge_request: 24997 +author: Rajendra Kadam +type: added diff --git a/changelogs/unreleased/remove-self-monitoring-feature-flag.yml b/changelogs/unreleased/remove-self-monitoring-feature-flag.yml new file mode 100644 index 00000000000..2362bfaf5d1 --- /dev/null +++ b/changelogs/unreleased/remove-self-monitoring-feature-flag.yml @@ -0,0 +1,5 @@ +--- +title: Remove self monitoring feature flag +merge_request: 23631 +author: +type: other diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md index 27cd0370bba..73a1dd8ad8a 100644 --- a/doc/development/go_guide/index.md +++ b/doc/development/go_guide/index.md @@ -72,6 +72,7 @@ projects: effects if the package is included multiple times. - Use `go fmt` before committing ([Gofmt](https://golang.org/cmd/gofmt/) is a tool that automatically formats Go source code). +- Place private methods below the first caller method in the source file. ### Automatic linting diff --git a/lib/api/entities.rb b/lib/api/entities.rb index df7c7b15aeb..3435d1d6f11 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -199,50 +199,6 @@ module API end.compact end end - - class UserAgentDetail < Grape::Entity - expose :user_agent - expose :ip_address - expose :submitted, as: :akismet_submitted - end - - class CustomAttribute < Grape::Entity - expose :key - expose :value - end - - class PagesDomainCertificateExpiration < Grape::Entity - expose :expired?, as: :expired - expose :expiration - end - - class Application < Grape::Entity - expose :id - expose :uid, as: :application_id - expose :name, as: :application_name - expose :redirect_uri, as: :callback_url - expose :confidential - end - - # Use with care, this exposes the secret - class ApplicationWithSecret < Application - expose :secret - end - - class Blob < Grape::Entity - expose :basename - expose :data - expose :path - # TODO: :filename was renamed to :path but both still return the full path, - # in the future we can only return the filename here without the leading - # directory path. - # https://gitlab.com/gitlab-org/gitlab/issues/34521 - expose :filename, &:path - expose :id - expose :ref - expose :startline - expose :project_id - end end end diff --git a/lib/api/entities/application.rb b/lib/api/entities/application.rb new file mode 100644 index 00000000000..33514200424 --- /dev/null +++ b/lib/api/entities/application.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class Application < Grape::Entity + expose :id + expose :uid, as: :application_id + expose :name, as: :application_name + expose :redirect_uri, as: :callback_url + expose :confidential + end + end +end diff --git a/lib/api/entities/application_with_secret.rb b/lib/api/entities/application_with_secret.rb new file mode 100644 index 00000000000..3e540381d89 --- /dev/null +++ b/lib/api/entities/application_with_secret.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + # Use with care, this exposes the secret + class ApplicationWithSecret < Entities::Application + expose :secret + end + end +end diff --git a/lib/api/entities/blob.rb b/lib/api/entities/blob.rb new file mode 100644 index 00000000000..b14ef127b68 --- /dev/null +++ b/lib/api/entities/blob.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module API + module Entities + class Blob < Grape::Entity + expose :basename + expose :data + expose :path + # TODO: :filename was renamed to :path but both still return the full path, + # in the future we can only return the filename here without the leading + # directory path. + # https://gitlab.com/gitlab-org/gitlab/issues/34521 + expose :filename, &:path + expose :id + expose :ref + expose :startline + expose :project_id + end + end +end diff --git a/lib/api/entities/custom_attribute.rb b/lib/api/entities/custom_attribute.rb new file mode 100644 index 00000000000..f949b709517 --- /dev/null +++ b/lib/api/entities/custom_attribute.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class CustomAttribute < Grape::Entity + expose :key + expose :value + end + end +end diff --git a/lib/api/entities/pages_domain_certificate_expiration.rb b/lib/api/entities/pages_domain_certificate_expiration.rb new file mode 100644 index 00000000000..bfc70f6657f --- /dev/null +++ b/lib/api/entities/pages_domain_certificate_expiration.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class PagesDomainCertificateExpiration < Grape::Entity + expose :expired?, as: :expired + expose :expiration + end + end +end diff --git a/lib/api/entities/user_agent_detail.rb b/lib/api/entities/user_agent_detail.rb new file mode 100644 index 00000000000..a2d02c16589 --- /dev/null +++ b/lib/api/entities/user_agent_detail.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class UserAgentDetail < Grape::Entity + expose :user_agent + expose :ip_address + expose :submitted, as: :akismet_submitted + end + end +end diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb index de7fdc27243..3531abb2604 100644 --- a/lib/api/group_import.rb +++ b/lib/api/group_import.rb @@ -5,15 +5,25 @@ module API MAXIMUM_FILE_SIZE = 50.megabytes.freeze helpers do - def authorize_create_group! - parent_group = find_group!(params[:parent_id]) if params[:parent_id].present? + def parent_group + find_group!(params[:parent_id]) if params[:parent_id].present? + end + def authorize_create_group! if parent_group authorize! :create_subgroup, parent_group else authorize! :create_group end end + + def closest_allowed_visibility_level + if parent_group + Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level) + else + Gitlab::VisibilityLevel::PRIVATE + end + end end resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do @@ -59,6 +69,7 @@ module API path: params[:path], name: params[:name], parent_id: params[:parent_id], + visibility_level: closest_allowed_visibility_level, import_export_upload: ImportExportUpload.new(import_file: uploaded_file) } diff --git a/lib/gitlab/import_export/group_import_export.yml b/lib/gitlab/import_export/group_import_export.yml index 7dcfbed8968..7203c1aa216 100644 --- a/lib/gitlab/import_export/group_import_export.yml +++ b/lib/gitlab/import_export/group_import_export.yml @@ -37,6 +37,7 @@ excluded_attributes: - :runners_token - :runners_token_encrypted - :saml_discovery_token + - :visibility_level methods: labels: diff --git a/lib/gitlab/import_export/group_tree_restorer.rb b/lib/gitlab/import_export/group_tree_restorer.rb index 8230e4ff128..2f42843ed6c 100644 --- a/lib/gitlab/import_export/group_tree_restorer.rb +++ b/lib/gitlab/import_export/group_tree_restorer.rb @@ -74,12 +74,23 @@ module Gitlab group_params = { name: group_hash['name'], path: group_hash['path'], - parent_id: parent_group&.id + parent_id: parent_group&.id, + visibility_level: sub_group_visibility_level(group_hash, parent_group) } ::Groups::CreateService.new(@user, group_params).execute end + def sub_group_visibility_level(group_hash, parent_group) + original_visibility_level = group_hash['visibility_level'] || Gitlab::VisibilityLevel::PRIVATE + + if parent_group && parent_group.visibility_level < original_visibility_level + Gitlab::VisibilityLevel.closest_allowed_level(parent_group.visibility_level) + else + original_visibility_level + end + end + def members_mapper @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @group_members, user: @user, importable: @group) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f8d3c389bf7..b429e0d99d5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10130,6 +10130,9 @@ msgstr "" msgid "IDE|Successful commit" msgstr "" +msgid "IDE|This option is disabled because you are not allowed to create merge requests in this project." +msgstr "" + msgid "IDE|This option is disabled because you don't have write permissions for the current branch." msgstr "" @@ -17112,9 +17115,6 @@ msgstr "" msgid "Self monitoring project does not exist" msgstr "" -msgid "Self-monitoring is not enabled on this GitLab server, contact your administrator." -msgstr "" - msgid "Self-monitoring project does not exist. Please check logs for any error messages" msgstr "" diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index c619bd6d6a3..61047c42fcc 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -7,6 +7,14 @@ module QA include Page::Component::ClonePanel include Page::Project::SubMenus::Settings + view 'app/assets/javascripts/repository/components/table/row.vue' do + element :file_name_link + end + + view 'app/assets/javascripts/repository/components/table/index.vue' do + element :file_tree_table + end + view 'app/views/layouts/header/_new_dropdown.haml' do element :new_menu_toggle element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern @@ -17,7 +25,8 @@ module QA end view 'app/views/projects/_home_panel.html.haml' do - element :project_name + element :forked_from_link + element :project_name_content end view 'app/views/projects/_files.html.haml' do @@ -37,10 +46,6 @@ module QA element :quick_actions end - view 'app/views/projects/tree/_tree_content.html.haml' do - element :file_tree - end - view 'app/views/projects/tree/_tree_header.html.haml' do element :add_to_tree element :new_file_option @@ -79,14 +84,18 @@ module QA click_on 'Fork' end + def forked_from?(parent_project_name) + has_element?(:forked_from_link, text: parent_project_name) + end + def click_file(filename) - within_element(:file_tree) do + within_element(:file_tree_table) do click_on filename end end def click_commit(commit_msg) - within_element(:file_tree) do + within_element(:file_tree_table) do click_on commit_msg end end @@ -96,6 +105,16 @@ module QA click_link 'New issue' end + def has_file?(name) + within_element(:file_tree_table) do + has_element?(:file_name_link, text: name) + end + end + + def has_name?(name) + has_element?(:project_name_content, text: name) + end + def last_commit_content find_element(:commit_content).text end @@ -113,7 +132,7 @@ module QA end def project_name - find('.qa-project-name').text + find_element(:project_name_content).text end def switch_to_branch(branch_name) diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index 3c06f139738..cac58c599ea 100644 --- a/qa/qa/resource/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -8,10 +8,12 @@ module QA module ApiFabricator include Capybara::DSL - ResourceNotFoundError = Class.new(RuntimeError) ResourceFabricationFailedError = Class.new(RuntimeError) - ResourceURLMissingError = Class.new(RuntimeError) ResourceNotDeletedError = Class.new(RuntimeError) + ResourceNotFoundError = Class.new(RuntimeError) + ResourceQueryError = Class.new(RuntimeError) + ResourceUpdateFailedError = Class.new(RuntimeError) + ResourceURLMissingError = Class.new(RuntimeError) attr_reader :api_resource, :api_response attr_writer :api_client diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb index 73f1b0b9695..854dd92e89c 100644 --- a/qa/qa/resource/fork.rb +++ b/qa/qa/resource/fork.rb @@ -3,19 +3,24 @@ module QA module Resource class Fork < Base + attribute :name do + upstream.name + end + attribute :project do - Resource::Project.fabricate! do |resource| - resource.name = upstream.project.name - resource.path_with_namespace = "#{user.name}/#{upstream.project.name}" + Resource::Project.fabricate_via_api! do |resource| + resource.add_name_uuid = false + resource.name = name + resource.path_with_namespace = "#{user.username}/#{name}" end end attribute :upstream do - Repository::ProjectPush.fabricate! + Repository::ProjectPush.fabricate!.project end attribute :user do - User.fabricate! do |resource| + User.fabricate_via_api! do |resource| if Runtime::Env.forker? resource.username = Runtime::Env.forker_username resource.password = Runtime::Env.forker_password @@ -33,7 +38,7 @@ module QA login.sign_in_using_credentials(user: user) end - upstream.project.visit! + upstream.visit! Page::Project::Show.perform(&:fork_project) @@ -47,6 +52,41 @@ module QA populate(:project) end + + def fabricate_via_api! + populate(:upstream, :user) + + Runtime::Logger.debug("Forking project #{upstream.name} to namespace #{user.username}...") + super + wait_until_forked + + populate(:project) + end + + def api_get_path + "/projects/#{CGI.escape(path_with_namespace)}" + end + + def api_post_path + "/projects/#{upstream.id}/fork" + end + + def api_post_body + { + namespace: user.username, + name: name, + path: name + } + end + + def wait_until_forked + Runtime::Logger.debug("Waiting for the fork process to complete...") + forked = wait_until do + project.import_status == "finished" + end + + raise "Timed out while waiting for the fork process to complete." unless forked + end end end end diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb index 9cb4e6a49ca..d9c86b3b527 100644 --- a/qa/qa/resource/merge_request_from_fork.rb +++ b/qa/qa/resource/merge_request_from_fork.rb @@ -8,7 +8,7 @@ module QA attr_accessor :fork_branch attribute :fork do - Fork.fabricate! + Fork.fabricate_via_browser_ui! end attribute :push do diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index f2ca0e0b8fd..62e55e18e9b 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -94,6 +94,10 @@ module QA "#{api_get_path}/runners" end + def api_put_path + "/projects/#{id}" + end + def api_post_path '/projects' end @@ -115,6 +119,35 @@ module QA post_body end + def change_repository_storage(new_storage) + put_body = { repository_storage: new_storage } + response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body + + unless response.code == HTTP_STATUS_OK + raise ResourceUpdateFailedError, "Could not change repository storage to #{new_storage}. Request returned (#{response.code}): `#{response}`." + end + + wait_until do + reload! + + api_response[:repository_storage] == new_storage + end + end + + def import_status + response = get Runtime::API::Request.new(api_client, "/projects/#{id}/import").url + + unless response.code == HTTP_STATUS_OK + raise ResourceQueryError, "Could not get import status. Request returned (#{response.code}): `#{response}`." + end + + result = parse_body(response) + + Runtime::Logger.error("Import failed: #{result[:import_error]}") if result[:import_status] == "failed" + + result[:import_status] + end + def runners(tag_list: nil) response = get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url parse_body(response) diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 6514e41e279..1c947b0329f 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -22,6 +22,10 @@ module QA SUPPORTED_FEATURES end + def additional_repository_storage + ENV['QA_ADDITIONAL_REPOSITORY_STORAGE'] + end + def admin_password ENV['GITLAB_ADMIN_PASSWORD'] end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 370bf30f3a4..7d4e6b7efbc 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -6,7 +6,7 @@ module QA it 'user forks a project, submits a merge request and maintainer merges it' do Flow::Login.sign_in - merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request| + merge_request = Resource::MergeRequestFromFork.fabricate_via_browser_ui! do |merge_request| merge_request.fork_branch = 'feature-branch' end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb new file mode 100644 index 00000000000..13fe8918f97 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin, quarantine: { type: :new } do + let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } + let(:parent_project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'parent-project' + project.initialize_with_readme = true + end + end + let(:fork_project) do + Resource::Fork.fabricate_via_api! do |fork| + fork.user = user + fork.upstream = parent_project + end.project + end + + before do + parent_project.add_member(user) + end + + it 'creates a 2nd fork after moving the parent project' do + Flow::Login.sign_in(as: user) + + fork_project.visit! + + parent_project.change_repository_storage(QA::Runtime::Env.additional_repository_storage) + + second_fork_project = Resource::Fork.fabricate_via_api! do |fork| + fork.name = "second-fork" + fork.user = user + fork.upstream = parent_project + end.project + + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = second_fork_project + push.file_name = 'new_file' + push.file_content = '# This is a new file' + push.commit_message = 'Add new file' + push.new_branch = false + end.project.visit! + + Page::Project::Show.perform do |show| + expect(show).to have_file('new_file') + expect(show).to have_name(second_fork_project.name) + expect(show).to be_forked_from(parent_project.name) + end + end + end + end +end diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb index ff133b58f89..63fcec4f9b3 100644 --- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb +++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb @@ -3,66 +3,96 @@ require 'spec_helper' describe 'Projects > Show > Collaboration links', :js do - let(:project) { create(:project, :repository) } + using RSpec::Parameterized::TableSyntax + + let(:project) { create(:project, :repository, :public) } let(:user) { create(:user) } before do - project.add_developer(user) sign_in(user) end - it 'shows all the expected links' do - visit project_path(project) + context 'with developer user' do + before do + project.add_developer(user) + end - # The navigation bar - page.within('.header-new') do - find('.qa-new-menu-toggle').click + it 'shows all the expected links' do + visit project_path(project) - aggregate_failures 'dropdown links in the navigation bar' do - expect(page).to have_link('New issue') - expect(page).to have_link('New merge request') - expect(page).to have_link('New snippet', href: new_project_snippet_path(project)) - end + # The navigation bar + page.within('.header-new') do + find('.qa-new-menu-toggle').click - find('.qa-new-menu-toggle').click - end + aggregate_failures 'dropdown links in the navigation bar' do + expect(page).to have_link('New issue') + expect(page).to have_link('New merge request') + expect(page).to have_link('New snippet', href: new_project_snippet_path(project)) + end - # The dropdown above the tree - page.within('.repo-breadcrumb') do - find('.qa-add-to-tree').click + find('.qa-new-menu-toggle').click + end - aggregate_failures 'dropdown links above the repo tree' do - expect(page).to have_link('New file') - expect(page).to have_link('Upload file') - expect(page).to have_link('New directory') - expect(page).to have_link('New branch') - expect(page).to have_link('New tag') + # The dropdown above the tree + page.within('.repo-breadcrumb') do + find('.qa-add-to-tree').click + + aggregate_failures 'dropdown links above the repo tree' do + expect(page).to have_link('New file') + expect(page).to have_link('Upload file') + expect(page).to have_link('New directory') + expect(page).to have_link('New branch') + expect(page).to have_link('New tag') + end end + + # The Web IDE + expect(page).to have_link('Web IDE') end - # The Web IDE - expect(page).to have_link('Web IDE') - end + it 'hides the links when the project is archived' do + project.update!(archived: true) - it 'hides the links when the project is archived' do - project.update!(archived: true) + visit project_path(project) - visit project_path(project) + page.within('.header-new') do + find('.qa-new-menu-toggle').click - page.within('.header-new') do - find('.qa-new-menu-toggle').click + aggregate_failures 'dropdown links' do + expect(page).not_to have_link('New issue') + expect(page).not_to have_link('New merge request') + expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project)) + end - aggregate_failures 'dropdown links' do - expect(page).not_to have_link('New issue') - expect(page).not_to have_link('New merge request') - expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project)) + find('.qa-new-menu-toggle').click end - find('.qa-new-menu-toggle').click + expect(page).not_to have_selector('.qa-add-to-tree') + + expect(page).not_to have_link('Web IDE') end + end - expect(page).not_to have_selector('.qa-add-to-tree') + context "Web IDE link" do + where(:merge_requests_access_level, :user_level, :expect_ide_link) do + ::ProjectFeature::DISABLED | :guest | false + ::ProjectFeature::DISABLED | :developer | true + ::ProjectFeature::PRIVATE | :guest | false + ::ProjectFeature::PRIVATE | :developer | true + ::ProjectFeature::ENABLED | :guest | true + ::ProjectFeature::ENABLED | :developer | true + end - expect(page).not_to have_link('Web IDE') + with_them do + before do + project.project_feature.update!({ merge_requests_access_level: merge_requests_access_level }) + project.add_user(user, user_level) + visit project_path(project) + end + + it "updates Web IDE link" do + expect(page.has_link?('Web IDE')).to be(expect_ide_link) + end + end end end diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/group.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/group.json new file mode 100644 index 00000000000..f747088f87e --- /dev/null +++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/group.json @@ -0,0 +1,166 @@ +{ + "id": 283, + "name": "internal", + "path": "internal", + "owner_id": null, + "created_at": "2020-02-12T16:56:34.924Z", + "updated_at": "2020-02-12T16:56:38.710Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 10, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": null, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null, + "children": [ + { + "id": 284, + "name": "public", + "path": "public", + "owner_id": null, + "created_at": "2020-02-12T17:33:00.575Z", + "updated_at": "2020-02-12T17:33:00.575Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 20, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": 283, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null + }, + { + "id": 285, + "name": "internal", + "path": "internal", + "owner_id": null, + "created_at": "2020-02-12T17:33:00.575Z", + "updated_at": "2020-02-12T17:33:00.575Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 10, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": 283, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null + }, + { + "id": 286, + "name": "private", + "path": "private", + "owner_id": null, + "created_at": "2020-02-12T17:33:00.575Z", + "updated_at": "2020-02-12T17:33:00.575Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 0, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": 283, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null + } + ] +} diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/group.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/group.json new file mode 100644 index 00000000000..1328e596fa5 --- /dev/null +++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/group.json @@ -0,0 +1,166 @@ +{ + "id": 283, + "name": "private", + "path": "private", + "owner_id": null, + "created_at": "2020-02-12T16:56:34.924Z", + "updated_at": "2020-02-12T16:56:38.710Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 0, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": null, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null, + "children": [ + { + "id": 284, + "name": "public", + "path": "public", + "owner_id": null, + "created_at": "2020-02-12T17:33:00.575Z", + "updated_at": "2020-02-12T17:33:00.575Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 20, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": 283, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null + }, + { + "id": 285, + "name": "internal", + "path": "internal", + "owner_id": null, + "created_at": "2020-02-12T17:33:00.575Z", + "updated_at": "2020-02-12T17:33:00.575Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 10, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": 283, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null + }, + { + "id": 286, + "name": "private", + "path": "private", + "owner_id": null, + "created_at": "2020-02-12T17:33:00.575Z", + "updated_at": "2020-02-12T17:33:00.575Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 0, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": 283, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null + } + ] +} diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/group.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/group.json new file mode 100644 index 00000000000..29020e92004 --- /dev/null +++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/group.json @@ -0,0 +1,166 @@ +{ + "id": 283, + "name": "public", + "path": "public", + "owner_id": null, + "created_at": "2020-02-12T16:56:34.924Z", + "updated_at": "2020-02-12T16:56:38.710Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 20, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": null, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null, + "children": [ + { + "id": 284, + "name": "public", + "path": "public", + "owner_id": null, + "created_at": "2020-02-12T17:33:00.575Z", + "updated_at": "2020-02-12T17:33:00.575Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 20, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": 283, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null + }, + { + "id": 285, + "name": "internal", + "path": "internal", + "owner_id": null, + "created_at": "2020-02-12T17:33:00.575Z", + "updated_at": "2020-02-12T17:33:00.575Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 10, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": 283, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null + }, + { + "id": 286, + "name": "private", + "path": "private", + "owner_id": null, + "created_at": "2020-02-12T17:33:00.575Z", + "updated_at": "2020-02-12T17:33:00.575Z", + "description": "", + "avatar": { + "url": null + }, + "membership_lock": false, + "share_with_group_lock": false, + "visibility_level": 0, + "request_access_enabled": true, + "ldap_sync_status": "ready", + "ldap_sync_error": null, + "ldap_sync_last_update_at": null, + "ldap_sync_last_successful_update_at": null, + "ldap_sync_last_sync_at": null, + "lfs_enabled": null, + "parent_id": 283, + "shared_runners_minutes_limit": null, + "repository_size_limit": null, + "require_two_factor_authentication": false, + "two_factor_grace_period": 48, + "plan_id": null, + "project_creation_level": 2, + "trial_ends_on": null, + "file_template_project_id": null, + "custom_project_templates_group_id": null, + "auto_devops_enabled": null, + "extra_shared_runners_minutes_limit": null, + "last_ci_minutes_notification_at": null, + "last_ci_minutes_usage_notification_level": null, + "subgroup_creation_level": 1, + "emails_disabled": null, + "max_pages_size": null, + "max_artifacts_size": null, + "mentions_disabled": null + } + ] +} diff --git a/spec/fixtures/lsif.json.gz b/spec/fixtures/lsif.json.gz Binary files differindex 275a87e738b..3f74588cd7e 100644 --- a/spec/fixtures/lsif.json.gz +++ b/spec/fixtures/lsif.json.gz diff --git a/spec/frontend/ide/mock_data.js b/spec/frontend/ide/mock_data.js index a1b57dca6bc..472516b6a2c 100644 --- a/spec/frontend/ide/mock_data.js +++ b/spec/frontend/ide/mock_data.js @@ -18,6 +18,7 @@ export const projectData = { }, mergeRequests: {}, merge_requests_enabled: true, + userPermissions: {}, default_branch: 'master', }; diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js index 83a3cfe618b..55f174f4663 100644 --- a/spec/frontend/ide/services/index_spec.js +++ b/spec/frontend/ide/services/index_spec.js @@ -2,11 +2,17 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import services from '~/ide/services'; import Api from '~/api'; +import gqClient from '~/ide/services/gql'; import { escapeFileUrl } from '~/lib/utils/url_utility'; +import getUserPermissions from '~/ide/queries/getUserPermissions.query.graphql'; +import { projectData } from '../mock_data'; jest.mock('~/api'); +jest.mock('~/ide/services/gql'); -const TEST_PROJECT_ID = 'alice/wonderland'; +const TEST_NAMESPACE = 'alice'; +const TEST_PROJECT = 'wonderland'; +const TEST_PROJECT_ID = `${TEST_NAMESPACE}/${TEST_PROJECT}`; const TEST_BRANCH = 'master-patch-123'; const TEST_COMMIT_SHA = '123456789'; const TEST_FILE_PATH = 'README2.md'; @@ -111,4 +117,27 @@ describe('IDE services', () => { }, ); }); + + describe('getProjectData', () => { + it('combines gql and API requests', () => { + const gqlProjectData = { + userPermissions: { + bogus: true, + }, + }; + Api.project.mockReturnValue(Promise.resolve({ data: { ...projectData } })); + gqClient.query.mockReturnValue(Promise.resolve({ data: { project: gqlProjectData } })); + + return services.getProjectData(TEST_NAMESPACE, TEST_PROJECT).then(response => { + expect(response).toEqual({ data: { ...projectData, ...gqlProjectData } }); + expect(Api.project).toHaveBeenCalledWith(TEST_PROJECT_ID); + expect(gqClient.query).toHaveBeenCalledWith({ + query: getUserPermissions, + variables: { + projectPath: TEST_PROJECT_ID, + }, + }); + }); + }); + }); }); diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js index 21c5e886738..011be95c1d2 100644 --- a/spec/frontend/ide/stores/getters_spec.js +++ b/spec/frontend/ide/stores/getters_spec.js @@ -2,6 +2,8 @@ import * as getters from '~/ide/stores/getters'; import { createStore } from '~/ide/stores'; import { file } from '../helpers'; +const TEST_PROJECT_ID = 'test_project'; + describe('IDE store getters', () => { let localState; let localStore; @@ -398,4 +400,38 @@ describe('IDE store getters', () => { }, ); }); + + describe('findProjectPermissions', () => { + it('returns false if project not found', () => { + expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual({}); + }); + + it('finds permission in given project', () => { + const userPermissions = { + readMergeRequest: true, + createMergeRequestsIn: false, + }; + + localState.projects[TEST_PROJECT_ID] = { userPermissions }; + + expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toBe(userPermissions); + }); + }); + + describe.each` + getterName | permissionKey + ${'canReadMergeRequests'} | ${'readMergeRequest'} + ${'canCreateMergeRequests'} | ${'createMergeRequestIn'} + `('$getterName', ({ getterName, permissionKey }) => { + it.each([true, false])('finds permission for current project (%s)', val => { + localState.projects[TEST_PROJECT_ID] = { + userPermissions: { + [permissionKey]: val, + }, + }; + localState.currentProjectId = TEST_PROJECT_ID; + + expect(localStore.getters[getterName]).toBe(val); + }); + }); }); diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap index 49feae0884e..2d411fcce79 100644 --- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap +++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap @@ -15,6 +15,7 @@ exports[`Repository table row component renders table row 1`] = ` <a class="str-truncated" + data-qa-selector="file_name_link" href="https://test.com" > @@ -64,6 +65,7 @@ exports[`Repository table row component renders table row for path with special <a class="str-truncated" + data-qa-selector="file_name_link" href="https://test.com" > diff --git a/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js index 02caf689c50..7c0b4000229 100644 --- a/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { projectData, branches } from 'spec/ide/mock_data'; -import { resetStore } from 'spec/ide/helpers'; import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue'; -import store from '~/ide/stores'; -import consts from '../../../../../app/assets/javascripts/ide/stores/modules/commit/constants'; +import { createStore } from '~/ide/stores'; +import { PERMISSION_CREATE_MR } from '~/ide/constants'; +import consts from '~/ide/stores/modules/commit/constants'; describe('create new MR checkbox', () => { + let store; let vm; + const setMR = () => { vm.$store.state.currentMergeRequestId = '1'; vm.$store.state.projects[store.state.currentProjectId].mergeRequests[ @@ -15,6 +17,10 @@ describe('create new MR checkbox', () => { ] = { foo: 'bar' }; }; + const setPermissions = permissions => { + store.state.projects[store.state.currentProjectId].userPermissions = permissions; + }; + const createComponent = ({ currentBranchId = 'master', createNewBranch = false } = {}) => { const Component = Vue.extend(NewMergeRequestOption); @@ -25,20 +31,29 @@ describe('create new MR checkbox', () => { : consts.COMMIT_TO_CURRENT_BRANCH; vm.$store.state.currentBranchId = currentBranchId; - vm.$store.state.currentProjectId = 'abcproject'; - const proj = JSON.parse(JSON.stringify(projectData)); - proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId); - - Vue.set(vm.$store.state.projects, 'abcproject', proj); + store.state.projects.abcproject.branches[currentBranchId] = branches.find( + branch => branch.name === currentBranchId, + ); return vm.$mount(); }; + const findInput = () => vm.$el.querySelector('input[type="checkbox"]'); + const findLabel = () => vm.$el.querySelector('.js-ide-commit-new-mr'); + + beforeEach(() => { + store = createStore(); + + store.state.currentProjectId = 'abcproject'; + + const proj = JSON.parse(JSON.stringify(projectData)); + proj.userPermissions[PERMISSION_CREATE_MR] = true; + Vue.set(store.state.projects, 'abcproject', proj); + }); + afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); describe('for default branch', () => { @@ -160,6 +175,24 @@ describe('create new MR checkbox', () => { .then(done) .catch(done.fail); }); + + it('shows enablded checkbox', () => { + expect(findLabel().classList.contains('is-disabled')).toBe(false); + expect(findInput().disabled).toBe(false); + }); + }); + + describe('when user cannot create MR', () => { + beforeEach(() => { + setPermissions({ [PERMISSION_CREATE_MR]: false }); + + createComponent({ currentBranchId: 'regular' }); + }); + + it('disabled checkbox', () => { + expect(findLabel().classList.contains('is-disabled')).toBe(true); + expect(findInput().disabled).toBe(true); + }); }); it('dispatches toggleShouldCreateMR when clicking checkbox', () => { diff --git a/spec/javascripts/ide/components/nav_dropdown_button_spec.js b/spec/javascripts/ide/components/nav_dropdown_button_spec.js index 0d63869fba2..bbaf97164ea 100644 --- a/spec/javascripts/ide/components/nav_dropdown_button_spec.js +++ b/spec/javascripts/ide/components/nav_dropdown_button_spec.js @@ -2,62 +2,92 @@ import Vue from 'vue'; import { trimText } from 'spec/helpers/text_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue'; -import store from '~/ide/stores'; -import { resetStore } from '../helpers'; +import { createStore } from '~/ide/stores'; describe('NavDropdown', () => { const TEST_BRANCH_ID = 'lorem-ipsum-dolar'; const TEST_MR_ID = '12345'; - const Component = Vue.extend(NavDropdownButton); + let store; let vm; beforeEach(() => { - vm = mountComponentWithStore(Component, { store }); - - vm.$mount(); + store = createStore(); }); afterEach(() => { vm.$destroy(); - - resetStore(store); }); - it('renders empty placeholders, if state is falsey', () => { - expect(trimText(vm.$el.textContent)).toEqual('- -'); - }); + const createComponent = (props = {}) => { + vm = mountComponentWithStore(Vue.extend(NavDropdownButton), { props, store }); + vm.$mount(); + }; - it('renders branch name, if state has currentBranchId', done => { - vm.$store.state.currentBranchId = TEST_BRANCH_ID; + const findIcon = name => vm.$el.querySelector(`.ic-${name}`); + const findMRIcon = () => findIcon('merge-request'); + const findBranchIcon = () => findIcon('branch'); - vm.$nextTick() - .then(() => { - expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`); - }) - .then(done) - .catch(done.fail); - }); + describe('normal', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders empty placeholders, if state is falsey', () => { + expect(trimText(vm.$el.textContent)).toEqual('- -'); + }); - it('renders mr id, if state has currentMergeRequestId', done => { - vm.$store.state.currentMergeRequestId = TEST_MR_ID; + it('renders branch name, if state has currentBranchId', done => { + vm.$store.state.currentBranchId = TEST_BRANCH_ID; - vm.$nextTick() - .then(() => { - expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`); - }) - .then(done) - .catch(done.fail); + vm.$nextTick() + .then(() => { + expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`); + }) + .then(done) + .catch(done.fail); + }); + + it('renders mr id, if state has currentMergeRequestId', done => { + vm.$store.state.currentMergeRequestId = TEST_MR_ID; + + vm.$nextTick() + .then(() => { + expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`); + }) + .then(done) + .catch(done.fail); + }); + + it('renders branch and mr, if state has both', done => { + vm.$store.state.currentBranchId = TEST_BRANCH_ID; + vm.$store.state.currentMergeRequestId = TEST_MR_ID; + + vm.$nextTick() + .then(() => { + expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`); + }) + .then(done) + .catch(done.fail); + }); + + it('shows icons', () => { + expect(findBranchIcon()).toBeTruthy(); + expect(findMRIcon()).toBeTruthy(); + }); }); - it('renders branch and mr, if state has both', done => { - vm.$store.state.currentBranchId = TEST_BRANCH_ID; - vm.$store.state.currentMergeRequestId = TEST_MR_ID; + describe('with showMergeRequests false', () => { + beforeEach(() => { + createComponent({ showMergeRequests: false }); + }); + + it('shows single empty placeholder, if state is falsey', () => { + expect(trimText(vm.$el.textContent)).toEqual('-'); + }); - vm.$nextTick() - .then(() => { - expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`); - }) - .then(done) - .catch(done.fail); + it('shows only branch icon', () => { + expect(findBranchIcon()).toBeTruthy(); + expect(findMRIcon()).toBe(null); + }); }); }); diff --git a/spec/javascripts/ide/components/nav_dropdown_spec.js b/spec/javascripts/ide/components/nav_dropdown_spec.js index fe1d0ca371d..dfb4d03540f 100644 --- a/spec/javascripts/ide/components/nav_dropdown_spec.js +++ b/spec/javascripts/ide/components/nav_dropdown_spec.js @@ -3,6 +3,9 @@ import Vue from 'vue'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import store from '~/ide/stores'; import NavDropdown from '~/ide/components/nav_dropdown.vue'; +import { PERMISSION_READ_MR } from '~/ide/constants'; + +const TEST_PROJECT_ID = 'lorem-ipsum'; describe('IDE NavDropdown', () => { const Component = Vue.extend(NavDropdown); @@ -10,6 +13,12 @@ describe('IDE NavDropdown', () => { let $dropdown; beforeEach(() => { + store.state.currentProjectId = TEST_PROJECT_ID; + Vue.set(store.state.projects, TEST_PROJECT_ID, { + userPermissions: { + [PERMISSION_READ_MR]: true, + }, + }); vm = mountComponentWithStore(Component, { store }); $dropdown = $(vm.$el); @@ -21,6 +30,9 @@ describe('IDE NavDropdown', () => { vm.$destroy(); }); + const findIcon = name => vm.$el.querySelector(`.ic-${name}`); + const findMRIcon = () => findIcon('merge-request'); + it('renders nothing initially', () => { expect(vm.$el).not.toContainElement('.ide-nav-form'); }); @@ -47,4 +59,22 @@ describe('IDE NavDropdown', () => { .then(done) .catch(done.fail); }); + + it('renders merge request icon', () => { + expect(findMRIcon()).not.toBeNull(); + }); + + describe('when user cannot read merge requests', () => { + beforeEach(done => { + store.state.projects[TEST_PROJECT_ID].userPermissions = {}; + + vm.$nextTick() + .then(done) + .catch(done.fail); + }); + + it('does not render merge requests', () => { + expect(findMRIcon()).toBeNull(); + }); + }); }); diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js index 498e5142f0c..4917984cd42 100644 --- a/spec/javascripts/ide/stores/actions/merge_request_spec.js +++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js @@ -8,7 +8,7 @@ import actions, { openMergeRequest, } from '~/ide/stores/actions/merge_request'; import service from '~/ide/services'; -import { activityBarViews } from '~/ide/constants'; +import { activityBarViews, PERMISSION_READ_MR } from '~/ide/constants'; import { resetStore } from '../../helpers'; const TEST_PROJECT = 'abcproject'; @@ -23,6 +23,9 @@ describe('IDE store merge request actions', () => { store.state.projects[TEST_PROJECT] = { id: TEST_PROJECT_ID, mergeRequests: {}, + userPermissions: { + [PERMISSION_READ_MR]: true, + }, }; }); @@ -79,6 +82,19 @@ describe('IDE store merge request actions', () => { }) .catch(done.fail); }); + + it('does nothing if user cannot read MRs', done => { + store.state.projects[TEST_PROJECT].userPermissions[PERMISSION_READ_MR] = false; + + store + .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) + .then(() => { + expect(service.getProjectMergeRequests).not.toHaveBeenCalled(); + expect(store.state.currentMergeRequestId).toBe(''); + }) + .then(done) + .catch(done.fail); + }); }); describe('no merge requests for branch available case', () => { diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 056822bcfe5..fb8cb300209 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -7,7 +7,7 @@ import eventHub from '~/ide/eventhub'; import consts from '~/ide/stores/modules/commit/constants'; import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types'; import * as actions from '~/ide/stores/modules/commit/actions'; -import { commitActionTypes } from '~/ide/constants'; +import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants'; import testAction from '../../../../helpers/vuex_action_helper'; const TEST_COMMIT_SHA = '123456789'; @@ -313,6 +313,9 @@ describe('IDE commit module actions', () => { }, }, }, + userPermissions: { + [PERMISSION_CREATE_MR]: true, + }, }, }, }); diff --git a/spec/lib/gitlab/import_export/group_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group_tree_restorer_spec.rb index 9aa9471d155..d6bda7507c6 100644 --- a/spec/lib/gitlab/import_export/group_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/group_tree_restorer_spec.rb @@ -125,4 +125,31 @@ describe Gitlab::ImportExport::GroupTreeRestorer do end end end + + context 'group visibility levels' do + let(:user) { create(:user) } + let(:shared) { Gitlab::ImportExport::Shared.new(group) } + let(:group_tree_restorer) { described_class.new(user: user, shared: shared, group: group, group_hash: nil) } + + before do + setup_import_export_config(filepath) + + group_tree_restorer.restore + end + + shared_examples 'with visibility level' do |visibility_level, expected_visibilities| + context "when visibility level is #{visibility_level}" do + let(:group) { create(:group, visibility_level) } + let(:filepath) { "group_exports/visibility_levels/#{visibility_level}" } + + it "imports all subgroups as #{visibility_level}" do + expect(group.children.map(&:visibility_level)).to eq(expected_visibilities) + end + end + end + + include_examples 'with visibility level', :public, [20, 10, 0] + include_examples 'with visibility level', :private, [0, 0, 0] + include_examples 'with visibility level', :internal, [10, 10, 0] + end end diff --git a/spec/lib/gitlab/import_export/group_tree_saver_spec.rb b/spec/lib/gitlab/import_export/group_tree_saver_spec.rb index d6aea8d1e69..d5ab8ca1e8d 100644 --- a/spec/lib/gitlab/import_export/group_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/group_tree_saver_spec.rb @@ -80,7 +80,7 @@ describe Gitlab::ImportExport::GroupTreeSaver do end it 'saves the correct json' do - expect(saved_group_json).to include({ 'description' => 'description', 'visibility_level' => 20 }) + expect(saved_group_json).to include({ 'description' => 'description' }) end it 'has milestones' do diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb index 016ed6ff491..1594881677f 100644 --- a/spec/requests/api/group_import_spec.rb +++ b/spec/requests/api/group_import_spec.rb @@ -45,6 +45,14 @@ describe API::GroupImport do expect(response).to have_gitlab_http_status(202) end + it 'creates private group' do + expect { subject }.to change { Group.count }.by(1) + + group = Group.find_by(name: 'test-import-group') + + expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + context 'when importing to a parent group' do before do group.add_owner(user) @@ -59,6 +67,34 @@ describe API::GroupImport do expect(group.children.count).to eq(1) end + context 'when parent group is private or internal' do + let(:public_parent_group) { create(:group, :public) } + let(:internal_parent_group) { create(:group, :internal) } + + before do + public_parent_group.add_owner(user) + internal_parent_group.add_owner(user) + end + + it 'imports public group' do + params[:parent_id] = public_parent_group.id + + subject + + expect(response).to have_gitlab_http_status(202) + expect(public_parent_group.children.first.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'imports internal group' do + params[:parent_id] = internal_parent_group.id + + subject + + expect(response).to have_gitlab_http_status(202) + expect(internal_parent_group.children.first.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + end + context 'when parent group is invalid' do it 'returns 404 and does not create new group' do params[:parent_id] = 99999 diff --git a/spec/requests/api/lsif_data_spec.rb b/spec/requests/api/lsif_data_spec.rb index 2e0670ded95..214bc832cda 100644 --- a/spec/requests/api/lsif_data_spec.rb +++ b/spec/requests/api/lsif_data_spec.rb @@ -61,7 +61,11 @@ describe API::LsifData do 'end_line' => 8, 'start_char' => 13, 'start_line' => 8, - 'definition_url' => project_blob_path(project, "#{commit.id}/morestrings/reverse.go", anchor: 'L5') + 'definition_url' => project_blob_path(project, "#{commit.id}/morestrings/reverse.go", anchor: 'L5'), + 'hover' => [{ + 'language' => 'go', + 'value' => Gitlab::Highlight.highlight(nil, 'func Func2(i int) string', language: 'go') + }] }) end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 00af0937dd7..58190beb38a 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -267,8 +267,7 @@ describe API::MergeRequests do it 'returns an array of merge requests with any label when filtering by any label' do get api(endpoint_path, user), params: { labels: [" #{label.title} ", " #{label2.title} "] } - expect_successful_response_with_paginated_array - expect(json_response.length).to eq(1) + expect_paginated_array_response([merge_request.id]) expect(json_response.first['labels']).to eq([label2.title, label.title]) expect(json_response.first['id']).to eq(merge_request.id) end @@ -276,8 +275,7 @@ describe API::MergeRequests do it 'returns an array of merge requests with any label when filtering by any label' do get api(endpoint_path, user), params: { labels: ["#{label.title} , #{label2.title}"] } - expect_successful_response_with_paginated_array - expect(json_response.length).to eq(1) + expect_paginated_array_response([merge_request.id]) expect(json_response.first['labels']).to eq([label2.title, label.title]) expect(json_response.first['id']).to eq(merge_request.id) end @@ -285,17 +283,16 @@ describe API::MergeRequests do it 'returns an array of merge requests with any label when filtering by any label' do get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_ANY } - expect_successful_response_with_paginated_array - expect(json_response.length).to eq(1) + expect_paginated_array_response([merge_request.id]) expect(json_response.first['id']).to eq(merge_request.id) end it 'returns an array of merge requests without a label when filtering by no label' do get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_NONE } - expect_paginated_array_response([ + expect_response_contain_exactly( merge_request_merged.id, merge_request_locked.id, merge_request_closed.id - ]) + ) end end @@ -315,7 +312,7 @@ describe API::MergeRequests do get api(path, user) - expect_paginated_array_response([mr2.id]) + expect_response_contain_exactly(mr2.id) end context 'with ordering' do @@ -413,9 +410,9 @@ describe API::MergeRequests do it 'returns merge requests with the given source branch' do get api(endpoint_path, user), params: { source_branch: merge_request_closed.source_branch, state: 'all' } - expect_paginated_array_response([ + expect_response_contain_exactly( merge_request_merged.id, merge_request_locked.id, merge_request_closed.id - ]) + ) end end @@ -423,9 +420,9 @@ describe API::MergeRequests do it 'returns merge requests with the given target branch' do get api(endpoint_path, user), params: { target_branch: merge_request_closed.target_branch, state: 'all' } - expect_paginated_array_response([ + expect_response_contain_exactly( merge_request_merged.id, merge_request_locked.id, merge_request_closed.id - ]) + ) end end end @@ -448,10 +445,10 @@ describe API::MergeRequests do it 'returns an array of all merge requests' do get api('/merge_requests', user), params: { scope: 'all' } - expect_paginated_array_response([ + expect_response_contain_exactly( merge_request_merged.id, merge_request_locked.id, merge_request_closed.id, merge_request.id - ]) + ) end it "returns authentication error without any scope" do @@ -487,9 +484,10 @@ describe API::MergeRequests do it 'returns an array of all merge requests except unauthorized ones' do get api('/merge_requests', user), params: { scope: :all } - expect_paginated_array_response([ - merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id - ]) + expect_response_contain_exactly( + merge_request_merged.id, merge_request2.id, merge_request_locked.id, + merge_request_closed.id, merge_request.id + ) end it "returns an array of no merge_requests when wip=yes" do @@ -501,9 +499,10 @@ describe API::MergeRequests do it "returns an array of no merge_requests when wip=no" do get api("/merge_requests", user), params: { wip: 'no' } - expect_paginated_array_response([ - merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id - ]) + expect_response_contain_exactly( + merge_request_merged.id, merge_request2.id, merge_request_locked.id, + merge_request_closed.id, merge_request.id + ) end it 'does not return unauthorized merge requests' do @@ -512,9 +511,10 @@ describe API::MergeRequests do get api('/merge_requests', user), params: { scope: :all } - expect_paginated_array_response([ - merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id - ]) + expect_response_contain_exactly( + merge_request_merged.id, merge_request2.id, merge_request_locked.id, + merge_request_closed.id, merge_request.id + ) expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id) end @@ -523,7 +523,7 @@ describe API::MergeRequests do get api('/merge_requests', user2) - expect_paginated_array_response([merge_request3.id]) + expect_response_contain_exactly(merge_request3.id) end it 'returns an array of merge requests authored by the given user' do @@ -531,7 +531,7 @@ describe API::MergeRequests do get api('/merge_requests', user), params: { author_id: user2.id, scope: :all } - expect_paginated_array_response([merge_request3.id]) + expect_response_contain_exactly(merge_request3.id) end it 'returns an array of merge requests assigned to the given user' do @@ -539,7 +539,7 @@ describe API::MergeRequests do get api('/merge_requests', user), params: { assignee_id: user2.id, scope: :all } - expect_paginated_array_response([merge_request3.id]) + expect_response_contain_exactly(merge_request3.id) end it 'returns an array of merge requests with no assignee' do @@ -547,7 +547,7 @@ describe API::MergeRequests do get api('/merge_requests', user), params: { assignee_id: 'None', scope: :all } - expect_paginated_array_response([merge_request3.id]) + expect_response_contain_exactly(merge_request3.id) end it 'returns an array of merge requests with any assignee' do @@ -556,10 +556,10 @@ describe API::MergeRequests do get api('/merge_requests', user), params: { assignee_id: 'Any', scope: :all } - expect_paginated_array_response([ + expect_response_contain_exactly( merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id - ]) + ) end it 'returns an array of merge requests assigned to me' do @@ -567,7 +567,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), params: { scope: 'assigned_to_me' } - expect_paginated_array_response([merge_request3.id]) + expect_response_contain_exactly(merge_request3.id) end it 'returns an array of merge requests assigned to me (kebab-case)' do @@ -575,7 +575,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), params: { scope: 'assigned-to-me' } - expect_paginated_array_response([merge_request3.id]) + expect_response_contain_exactly(merge_request3.id) end it 'returns an array of merge requests created by me' do @@ -583,7 +583,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), params: { scope: 'created_by_me' } - expect_paginated_array_response([merge_request3.id]) + expect_response_contain_exactly(merge_request3.id) end it 'returns an array of merge requests created by me (kebab-case)' do @@ -591,7 +591,7 @@ describe API::MergeRequests do get api('/merge_requests', user2), params: { scope: 'created-by-me' } - expect_paginated_array_response([merge_request3.id]) + expect_response_contain_exactly(merge_request3.id) end it 'returns merge requests reacted by the authenticated user by the given emoji' do @@ -600,16 +600,16 @@ describe API::MergeRequests do get api('/merge_requests', user2), params: { my_reaction_emoji: award_emoji.name, scope: 'all' } - expect_paginated_array_response([merge_request3.id]) + expect_response_contain_exactly(merge_request3.id) end context 'source_branch param' do it 'returns merge requests with the given source branch' do get api('/merge_requests', user), params: { source_branch: merge_request_closed.source_branch, state: 'all' } - expect_paginated_array_response([ + expect_response_contain_exactly( merge_request_merged.id, merge_request_locked.id, merge_request_closed.id - ]) + ) end end @@ -617,9 +617,9 @@ describe API::MergeRequests do it 'returns merge requests with the given target branch' do get api('/merge_requests', user), params: { target_branch: merge_request_closed.target_branch, state: 'all' } - expect_paginated_array_response([ + expect_response_contain_exactly( merge_request_merged.id, merge_request_locked.id, merge_request_closed.id - ]) + ) end end @@ -628,7 +628,7 @@ describe API::MergeRequests do get api('/merge_requests?created_before=2000-01-02T00:00:00.060Z', user) - expect_paginated_array_response([merge_request2.id]) + expect_response_contain_exactly(merge_request2.id) end it 'returns merge requests created after a specific date' do @@ -636,7 +636,7 @@ describe API::MergeRequests do get api("/merge_requests?created_after=#{merge_request2.created_at}", user) - expect_paginated_array_response([merge_request2.id]) + expect_response_contain_exactly(merge_request2.id) end it 'returns merge requests updated before a specific date' do @@ -644,7 +644,7 @@ describe API::MergeRequests do get api('/merge_requests?updated_before=2000-01-02T00:00:00.060Z', user) - expect_paginated_array_response([merge_request2.id]) + expect_response_contain_exactly(merge_request2.id) end it 'returns merge requests updated after a specific date' do @@ -652,7 +652,7 @@ describe API::MergeRequests do get api("/merge_requests?updated_after=#{merge_request2.updated_at}", user) - expect_paginated_array_response([merge_request2.id]) + expect_response_contain_exactly(merge_request2.id) end context 'search params' do @@ -663,13 +663,13 @@ describe API::MergeRequests do it 'returns merge requests matching given search string for title' do get api("/merge_requests", user), params: { search: merge_request.title } - expect_paginated_array_response([merge_request.id]) + expect_response_contain_exactly(merge_request.id) end it 'returns merge requests matching given search string for title and scoped in title' do get api("/merge_requests", user), params: { search: merge_request.title, in: 'title' } - expect_paginated_array_response([merge_request.id]) + expect_response_contain_exactly(merge_request.id) end it 'returns an empty array if no merge request matches given search string for description and scoped in title' do @@ -681,7 +681,7 @@ describe API::MergeRequests do it 'returns merge requests for project matching given search string for description' do get api("/merge_requests", user), params: { project_id: project.id, search: merge_request.description } - expect_paginated_array_response([merge_request.id]) + expect_response_contain_exactly(merge_request.id) end end @@ -689,7 +689,7 @@ describe API::MergeRequests do it 'returns merge requests with the given state' do get api('/merge_requests', user), params: { state: 'locked' } - expect_paginated_array_response([merge_request_locked.id]) + expect_response_contain_exactly(merge_request_locked.id) end end end @@ -792,10 +792,10 @@ describe API::MergeRequests do it 'returns an array excluding merge_requests from archived projects' do get api(endpoint_path, user) - expect_paginated_array_response([ + expect_response_contain_exactly( merge_request_merged.id, merge_request_locked.id, merge_request_closed.id, merge_request.id - ]) + ) end context 'with non_archived param set as false' do @@ -804,10 +804,10 @@ describe API::MergeRequests do get api(path, user) - expect_paginated_array_response([ + expect_response_contain_exactly( merge_request_merged.id, merge_request_archived.id, merge_request_locked.id, merge_request_closed.id, merge_request.id - ]) + ) end end end diff --git a/spec/requests/self_monitoring_project_spec.rb b/spec/requests/self_monitoring_project_spec.rb index 5e46645e7a0..6a0258c349f 100644 --- a/spec/requests/self_monitoring_project_spec.rb +++ b/spec/requests/self_monitoring_project_spec.rb @@ -17,11 +17,7 @@ describe 'Self-Monitoring project requests' do login_as(admin) end - context 'with feature flag disabled' do - it_behaves_like 'not accessible if feature flag is disabled' - end - - context 'with feature flag enabled' do + context 'when the self monitoring project is created' do let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path } it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted' @@ -45,11 +41,7 @@ describe 'Self-Monitoring project requests' do login_as(admin) end - context 'with feature flag disabled' do - it_behaves_like 'not accessible if feature flag is disabled' - end - - context 'with feature flag enabled' do + context 'when the self monitoring project is being created' do it_behaves_like 'handles invalid job_id' context 'when job is in progress' do @@ -129,11 +121,7 @@ describe 'Self-Monitoring project requests' do login_as(admin) end - context 'with feature flag disabled' do - it_behaves_like 'not accessible if feature flag is disabled' - end - - context 'with feature flag enabled' do + context 'when the self monitoring project is deleted' do let(:status_api) { status_delete_self_monitoring_project_admin_application_settings_path } it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted' @@ -157,11 +145,7 @@ describe 'Self-Monitoring project requests' do login_as(admin) end - context 'with feature flag disabled' do - it_behaves_like 'not accessible if feature flag is disabled' - end - - context 'with feature flag enabled' do + context 'when the self monitoring project is being deleted' do it_behaves_like 'handles invalid job_id' context 'when job is in progress' do diff --git a/spec/services/projects/lsif_data_service_spec.rb b/spec/services/projects/lsif_data_service_spec.rb index 29a99a96c41..93579869d1d 100644 --- a/spec/services/projects/lsif_data_service_spec.rb +++ b/spec/services/projects/lsif_data_service_spec.rb @@ -12,6 +12,10 @@ describe Projects::LsifDataService do let(:service) { described_class.new(artifact.file, project, params) } describe '#execute' do + def highlighted_value(value) + [{ language: 'go', value: Gitlab::Highlight.highlight(nil, value, language: 'go') }] + end + context 'fetched lsif file', :use_clean_rails_memory_store_caching do it 'is cached' do service.execute @@ -32,42 +36,48 @@ describe Projects::LsifDataService do end_line: 6, start_char: 5, start_line: 6, - definition_url: "#{path_prefix}/main.go#L7" + definition_url: "#{path_prefix}/main.go#L7", + hover: highlighted_value('func main()') }, { end_char: 36, end_line: 3, start_char: 1, start_line: 3, - definition_url: "#{path_prefix}/main.go#L4" + definition_url: "#{path_prefix}/main.go#L4", + hover: highlighted_value('package "github.com/user/hello/morestrings" ("github.com/user/hello/morestrings")') }, { end_char: 12, end_line: 7, start_char: 1, start_line: 7, - definition_url: "#{path_prefix}/main.go#L4" + definition_url: "#{path_prefix}/main.go#L4", + hover: highlighted_value('package "github.com/user/hello/morestrings" ("github.com/user/hello/morestrings")') }, { end_char: 20, end_line: 7, start_char: 13, start_line: 7, - definition_url: "#{path_prefix}/morestrings/reverse.go#L11" + definition_url: "#{path_prefix}/morestrings/reverse.go#L11", + hover: highlighted_value('func Reverse(s string) string') + [{ value: "This method reverses a string \n\n" }] }, { end_char: 12, end_line: 8, start_char: 1, start_line: 8, - definition_url: "#{path_prefix}/main.go#L4" + definition_url: "#{path_prefix}/main.go#L4", + hover: highlighted_value('package "github.com/user/hello/morestrings" ("github.com/user/hello/morestrings")') }, { end_char: 18, end_line: 8, start_char: 13, start_line: 8, - definition_url: "#{path_prefix}/morestrings/reverse.go#L5" + definition_url: "#{path_prefix}/morestrings/reverse.go#L5", + hover: highlighted_value('func Func2(i int) string') } ]) end @@ -82,7 +92,8 @@ describe Projects::LsifDataService do end_line: 11, start_char: 1, start_line: 11, - definition_url: "/#{project.full_path}/-/blob/#{commit_id}/morestrings/reverse.go#L12" + definition_url: "/#{project.full_path}/-/blob/#{commit_id}/morestrings/reverse.go#L12", + hover: highlighted_value('var a string') }) end end diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb index 44c38df71b0..dc263d64bcc 100644 --- a/spec/support/helpers/api_helpers.rb +++ b/spec/support/helpers/api_helpers.rb @@ -58,6 +58,13 @@ module ApiHelpers expect(json_response.map { |item| item['id'] }).to eq(Array(items)) end + def expect_response_contain_exactly(*items) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(json_response.length).to eq(items.size) + expect(json_response.map { |item| item['id'] }).to contain_exactly(*items) + end + def stub_last_activity_update allow_any_instance_of(Users::ActivityService).to receive(:execute) end diff --git a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb index f6cb2555450..db11b1fe07d 100644 --- a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb +++ b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb @@ -1,23 +1,5 @@ # frozen_string_literal: true -RSpec.shared_examples 'not accessible if feature flag is disabled' do - before do - stub_feature_flags(self_monitoring_project: false) - end - - it 'returns not_implemented' do - subject - - aggregate_failures do - expect(response).to have_gitlab_http_status(:not_implemented) - expect(json_response).to eq( - 'message' => _('Self-monitoring is not enabled on this GitLab server, contact your administrator.'), - 'documentation_url' => help_page_path('administration/monitoring/gitlab_self_monitoring_project/index') - ) - end - end -end - RSpec.shared_examples 'not accessible to non-admin users' do context 'with unauthenticated user' do it 'redirects to signin page' do diff --git a/spec/views/projects/tree/_tree_header.html.haml_spec.rb b/spec/views/projects/tree/_tree_header.html.haml_spec.rb index caf8c4d1969..69ad331f880 100644 --- a/spec/views/projects/tree/_tree_header.html.haml_spec.rb +++ b/spec/views/projects/tree/_tree_header.html.haml_spec.rb @@ -19,12 +19,12 @@ describe 'projects/tree/_tree_header' do allow(view).to receive(:can_collaborate_with_project?) { true } end - it 'does not render the WebIDE button when user cannot create fork or cannot open MR' do + it 'renders the WebIDE button when user can collaborate but not create fork or MR' do allow(view).to receive(:can?) { false } render - expect(rendered).not_to have_link('Web IDE') + expect(rendered).to have_link('Web IDE') end it 'renders the WebIDE button when user can create fork and can open MR in project' do @@ -43,4 +43,13 @@ describe 'projects/tree/_tree_header' do expect(rendered).to have_link('Web IDE', href: '#modal-confirm-fork') end + + it 'does not render the WebIDE button when user cannot collaborate or create mr' do + allow(view).to receive(:can?) { false } + allow(view).to receive(:can_collaborate_with_project?) { false } + + render + + expect(rendered).not_to have_link('Web IDE') + end end |