Commit b3a736ed authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 5366964a
......@@ -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>
......
<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>
......
......@@ -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">
......
<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>
......@@ -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>
......
......@@ -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>
......@@ -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',
......
query getUserPermissions($projectPath: ID!) {
project(fullPath: $projectPath) {
userPermissions {
createMergeRequestIn,
readMergeRequest
}
}
}
import createGqClient, { fetchPolicies } from '~/lib/graphql';
export default createGqClient(
{},
{
fetchPolicy: fetchPolicies.NO_CACHE,
},
);
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);
......
......@@ -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 },
......
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 () => {};
......@@ -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
......
......@@ -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 () => {};
......@@ -3,9 +3,7 @@ import projectSelect from '~/project_select';
import selfMonitor from '~/self_monitor';
document.addEventListener('DOMContentLoaded', () => {
if (gon.features && gon.features.selfMonitoringProject) {
selfMonitor();
}
// Initialize expandable settings panels
initSettingsPanels();
projectSelect();
......
......@@ -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
......
......@@ -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 -->
......
......@@ -688,7 +688,7 @@ $ide-commit-header-height: 48px;
font-weight: normal;
&.is-disabled {
.ide-radio-label {
.ide-option-label {
text-decoration: line-through;
}
}
......
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -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')
......
.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')
......
......@@ -84,14 +84,13 @@
= 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')
- else
- 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)
......
---
title: Enable Web IDE on projects without Merge Requests
merge_request: 24508
author:
type: fixed
---
title: When a namespace GitLab Subscription expires, disable SSO enforcement
merge_request: 21135
author:
type: fixed
---
title: Use closest allowed visibility level on group creation when importing groups
using Group Import/Export
merge_request: 25026
author:
type: fixed
---
title: Separate entities into own class files
merge_request: 24985
author: Rajendra Kadam
type: added
---
title: Separate Application and Blob entities into own class files
merge_request: 24997
author: Rajendra Kadam
type: added
---
title: Remove self monitoring feature flag
merge_request: 23631
author:
type: other
......@@ -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
......
......@@ -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
......
# 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
# frozen_string_literal: true
module API
module Entities
# Use with care, this exposes the secret
class ApplicationWithSecret < Entities::Application
expose :secret
end
end
end
# 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
# frozen_string_literal: true
module API
module Entities
class CustomAttribute < Grape::Entity
expose :key
expose :value
end
end
end
# frozen_string_literal: true
module API
module Entities
class PagesDomainCertificateExpiration < Grape::Entity
expose :expired?, as: :expired
expose :expiration
end
end
end
# 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
......@@ -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)
}
......
......@@ -37,6 +37,7 @@ excluded_attributes:
- :runners_token
- :runners_token_encrypted
- :saml_discovery_token
- :visibility_level
methods:
labels:
......
......@@ -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
......
......@@ -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 ""
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......@@ -8,7 +8,7 @@ module QA
attr_accessor :fork_branch
attribute :fork do
Fork.fabricate!
Fork.fabricate_via_browser_ui!
end
attribute :push do
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......
# 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
......@@ -3,14 +3,20 @@
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
context 'with developer user' do
before do
project.add_developer(user)
end
it 'shows all the expected links' do
visit project_path(project)
......@@ -65,4 +71,28 @@ describe 'Projects > Show > Collaboration links', :js do
expect(page).not_to have_link('Web IDE')
end
end
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
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
{
"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
}
]
}
{
"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
}
]
}
{
"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
}
]
}
......@@ -18,6 +18,7 @@ export const projectData = {
},
mergeRequests: {},
merge_requests_enabled: true,
userPermissions: {},
default_branch: 'master',
};
......
......@@ -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,
},
});
});
});
});
});
......@@ -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);
});
});
});
......@@ -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"
>
......
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', () => {
......
......@@ -2,25 +2,34 @@ 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();
});
const createComponent = (props = {}) => {
vm = mountComponentWithStore(Vue.extend(NavDropdownButton), { props, store });
vm.$mount();
};
resetStore(store);
const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
const findMRIcon = () => findIcon('merge-request');
const findBranchIcon = () => findIcon('branch');
describe('normal', () => {
beforeEach(() => {
createComponent();
});
it('renders empty placeholders, if state is falsey', () => {
......@@ -60,4 +69,25 @@ describe('NavDropdown', () => {
.then(done)
.catch(done.fail);
});
it('shows icons', () => {
expect(findBranchIcon()).toBeTruthy();
expect(findMRIcon()).toBeTruthy();
});
});
describe('with showMergeRequests false', () => {
beforeEach(() => {
createComponent({ showMergeRequests: false });
});
it('shows single empty placeholder, if state is falsey', () => {
expect(trimText(vm.$el.textContent)).toEqual('-');
});
it('shows only branch icon', () => {
expect(findBranchIcon()).toBeTruthy();
expect(findMRIcon()).toBe(null);
});
});
});
......@@ -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();
});
});
});
......@@ -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', () => {
......
......@@ -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,
},
},
},
});
......
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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
......
This diff is collapsed.
......@@ -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
......
......@@ -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
......
......@@ -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
......
# 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
......
......@@ -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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment