summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2019-07-05 11:14:56 +0100
committerPhil Hughes <me@iamphill.com>2019-07-05 12:16:46 +0100
commit77c35d5d001a0ce0626bc8aeec574eca36c2233b (patch)
tree2a98e40e56b6d5820d8ee95a75675276e11b603b
parent9a4b5f08dbf5e0900145b5127f50e7ab3578d05c (diff)
downloadgitlab-ce-77c35d5d001a0ce0626bc8aeec574eca36c2233b.tar.gz
Create private merge requests in forks
https://gitlab.com/gitlab-org/gitlab-ce/issues/58583
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/dropdown.vue58
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue136
-rw-r--r--app/assets/javascripts/confidential_merge_request/index.js30
-rw-r--r--app/assets/javascripts/confidential_merge_request/state.js5
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js47
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/helpers/issues_helper.rb2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml13
-rw-r--r--locale/gitlab.pot18
-rw-r--r--spec/features/issues/user_creates_confidential_merge_request_spec.rb54
-rw-r--r--spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap101
-rw-r--r--spec/frontend/confidential_merge_request/components/dropdown_spec.js56
-rw-r--r--spec/frontend/confidential_merge_request/components/project_form_group_spec.js77
-rw-r--r--spec/frontend/create_merge_request_dropdown_spec.js (renamed from spec/javascripts/create_merge_request_dropdown_spec.js)9
16 files changed, 599 insertions, 15 deletions
diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
new file mode 100644
index 00000000000..444640980af
--- /dev/null
+++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ Icon,
+ },
+ props: {
+ projects: {
+ type: Array,
+ required: true,
+ },
+ selectedProject: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ dropdownText() {
+ if (Object.keys(this.selectedProject).length) {
+ return this.selectedProject.name;
+ }
+
+ return __('Select private project');
+ },
+ },
+ methods: {
+ selectProject(project) {
+ this.$emit('click', project);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown toggle-class="d-flex align-items-center w-100" class="w-100">
+ <template slot="button-content">
+ <span class="str-truncated-100 mr-2">
+ <icon name="lock" />
+ {{ dropdownText }}
+ </span>
+ <icon name="chevron-down" class="ml-auto" />
+ </template>
+ <gl-dropdown-item v-for="project in projects" :key="project.id" @click="selectProject(project)">
+ <icon
+ name="mobile-issue-close"
+ :class="{ icon: project.id !== selectedProject.id }"
+ class="js-active-project-check"
+ />
+ <span class="ml-1">{{ project.name }}</span>
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
new file mode 100644
index 00000000000..b89729375be
--- /dev/null
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -0,0 +1,136 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { __, sprintf } from '../../locale';
+import createFlash from '../../flash';
+import Api from '../../api';
+import state from '../state';
+import Dropdown from './dropdown.vue';
+
+export default {
+ components: {
+ GlLink,
+ Dropdown,
+ },
+ props: {
+ namespacePath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ newForkPath: {
+ type: String,
+ required: true,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ projects: [],
+ };
+ },
+ computed: {
+ selectedProject() {
+ return state.selectedProject;
+ },
+ noForkText() {
+ return sprintf(
+ __(
+ 'To protect this issues confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private.',
+ ),
+ { link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' },
+ false,
+ );
+ },
+ },
+ mounted() {
+ this.fetchProjects();
+ this.createBtn = document.querySelector('.js-create-target');
+ this.warningText = document.querySelector('.js-exposed-info-warning');
+ },
+ methods: {
+ selectProject(project) {
+ if (project) {
+ Object.assign(state, {
+ selectedProject: project,
+ });
+
+ if (project.namespaceFullPath !== this.namespacePath) {
+ this.showWarning();
+ }
+ } else if (this.createBtn) {
+ this.createBtn.setAttribute('disabled', 'disabled');
+ }
+ },
+ normalizeProjectData(data) {
+ return data.map(p => ({
+ id: p.id,
+ name: p.name_with_namespace,
+ pathWithNamespace: p.path_with_namespace,
+ namespaceFullpath: p.namespace.full_path,
+ }));
+ },
+ fetchProjects() {
+ Api.projectForks(this.projectPath, {
+ with_merge_requests_enabled: true,
+ min_access_level: 30,
+ visibility: 'private',
+ })
+ .then(({ data }) => {
+ this.projects = this.normalizeProjectData(data);
+ this.selectProject(this.projects[0]);
+ })
+ .catch(e => {
+ createFlash(__('Error fetching forked projects. Please try again.'));
+ throw e;
+ });
+ },
+ showWarning() {
+ if (this.warningText) {
+ this.warningText.classList.remove('hidden');
+ }
+
+ if (this.createBtn) {
+ this.createBtn.classList.add('btn-warning');
+ this.createBtn.classList.remove('btn-success');
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="form-group">
+ <label>{{ __('Project') }}</label>
+ <div>
+ <dropdown
+ v-if="projects.length"
+ :projects="projects"
+ :selected-project="selectedProject"
+ @click="selectProject"
+ />
+ <p class="text-muted mt-1 mb-0">
+ <template v-if="projects.length">
+ {{
+ __(
+ 'To protect this issues confidentiality, a private fork of this project was selected.',
+ )
+ }}
+ </template>
+ <template v-else>
+ {{ __('No forks available to you.') }}<br />
+ <span v-html="noForkText"></span>
+ </template>
+ <gl-link :href="helpPagePath" class="help-link" target="_blank">
+ <span class="sr-only">{{ __('Read more') }}</span>
+ <i class="fa fa-question-circle" aria-hidden="true"></i>
+ </gl-link>
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/confidential_merge_request/index.js b/app/assets/javascripts/confidential_merge_request/index.js
new file mode 100644
index 00000000000..9672821d30e
--- /dev/null
+++ b/app/assets/javascripts/confidential_merge_request/index.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import { parseBoolean } from '../lib/utils/common_utils';
+import ProjectFormGroup from './components/project_form_group.vue';
+import state from './state';
+
+export function isConfidentialIssue() {
+ return parseBoolean(document.querySelector('.js-create-mr').dataset.isConfidential);
+}
+
+export function canCreateConfidentialMergeRequest() {
+ return isConfidentialIssue() && Object.keys(state.selectedProject).length > 0;
+}
+
+export function init() {
+ const el = document.getElementById('js-forked-project');
+
+ return new Vue({
+ el,
+ render(h) {
+ return h(ProjectFormGroup, {
+ props: {
+ namespacePath: el.dataset.namespacePath,
+ projectPath: el.dataset.projectPath,
+ newForkPath: el.dataset.newForkPath,
+ helpPagePath: el.dataset.helpPagePath,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/confidential_merge_request/state.js b/app/assets/javascripts/confidential_merge_request/state.js
new file mode 100644
index 00000000000..95b0580f4b9
--- /dev/null
+++ b/app/assets/javascripts/confidential_merge_request/state.js
@@ -0,0 +1,5 @@
+import Vue from 'vue';
+
+export default Vue.observable({
+ selectedProject: {},
+});
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index 8f5cece0788..052168bb21c 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -5,6 +5,12 @@ import Flash from './flash';
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
import { __, sprintf } from './locale';
+import {
+ init as initConfidentialMergeRequest,
+ isConfidentialIssue,
+ canCreateConfidentialMergeRequest,
+} from './confidential_merge_request';
+import confidentialMergeRequestState from './confidential_merge_request/state';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = Object.assign({}, ISetter);
@@ -12,6 +18,17 @@ const InputSetter = Object.assign({}, ISetter);
const CREATE_MERGE_REQUEST = 'create-mr';
const CREATE_BRANCH = 'create-branch';
+function createEndpoint(projectPath, endpoint) {
+ if (canCreateConfidentialMergeRequest()) {
+ return endpoint.replace(
+ projectPath,
+ confidentialMergeRequestState.selectedProject.pathWithNamespace,
+ );
+ }
+
+ return endpoint;
+}
+
export default class CreateMergeRequestDropdown {
constructor(wrapperEl) {
this.wrapperEl = wrapperEl;
@@ -42,6 +59,8 @@ export default class CreateMergeRequestDropdown {
this.refIsValid = true;
this.refsPath = this.wrapperEl.dataset.refsPath;
this.suggestedRef = this.refInput.value;
+ this.projectPath = this.wrapperEl.dataset.projectPath;
+ this.projectId = this.wrapperEl.dataset.projectId;
// These regexps are used to replace
// a backend generated new branch name and its source (ref)
@@ -58,6 +77,14 @@ export default class CreateMergeRequestDropdown {
};
this.init();
+
+ if (isConfidentialIssue()) {
+ this.createMergeRequestButton.setAttribute(
+ 'data-dropdown-trigger',
+ '#create-merge-request-dropdown',
+ );
+ initConfidentialMergeRequest();
+ }
}
available() {
@@ -113,7 +140,9 @@ export default class CreateMergeRequestDropdown {
this.isCreatingBranch = true;
return axios
- .post(this.createBranchPath)
+ .post(createEndpoint(this.projectPath, this.createBranchPath), {
+ confidential_issue_project_id: canCreateConfidentialMergeRequest() ? this.projectId : null,
+ })
.then(({ data }) => {
this.branchCreated = true;
window.location.href = data.url;
@@ -125,7 +154,11 @@ export default class CreateMergeRequestDropdown {
this.isCreatingMergeRequest = true;
return axios
- .post(this.createMrPath)
+ .post(this.createMrPath, {
+ target_project_id: canCreateConfidentialMergeRequest()
+ ? confidentialMergeRequestState.selectedProject.id
+ : null,
+ })
.then(({ data }) => {
this.mergeRequestCreated = true;
window.location.href = data.url;
@@ -149,6 +182,8 @@ export default class CreateMergeRequestDropdown {
}
enable() {
+ if (!canCreateConfidentialMergeRequest()) return;
+
this.createMergeRequestButton.classList.remove('disabled');
this.createMergeRequestButton.removeAttribute('disabled');
@@ -205,7 +240,7 @@ export default class CreateMergeRequestDropdown {
if (!ref) return false;
return axios
- .get(`${this.refsPath}${encodeURIComponent(ref)}`)
+ .get(`${createEndpoint(this.projectPath, this.refsPath)}${encodeURIComponent(ref)}`)
.then(({ data }) => {
const branches = data[Object.keys(data)[0]];
const tags = data[Object.keys(data)[1]];
@@ -325,6 +360,12 @@ export default class CreateMergeRequestDropdown {
let xhr = null;
event.preventDefault();
+ if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) {
+ this.droplab.hooks.forEach(hook => hook.list.toggle());
+
+ return;
+ }
+
if (this.isBusy()) {
return;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cd951f67293..a12029d2419 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -287,8 +287,8 @@
list-style: none;
padding: 0 1px;
- a,
- button,
+ a:not(.help-link),
+ button:not(.btn),
.menu-item {
@include dropdown-link;
}
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index d77f64a84f5..141a7dfb923 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -169,7 +169,7 @@ class Projects::BranchesController < Projects::ApplicationController
end
def confidential_issue_project
- return unless Feature.enabled?(:create_confidential_merge_request, @project)
+ return unless helpers.create_confidential_merge_request_enabled?
return if params[:confidential_issue_project_id].blank?
confidential_issue_project = Project.find(params[:confidential_issue_project_id])
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index e275b417784..b866f574f67 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -172,7 +172,7 @@ class Projects::IssuesController < Projects::ApplicationController
def create_merge_request
create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
- create_params[:target_project_id] = params[:target_project_id] if Feature.enabled?(:create_confidential_merge_request, @project)
+ create_params[:target_project_id] = params[:target_project_id] if helpers.create_confidential_merge_request_enabled?
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
if result[:status] == :success
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index dfadcfc33b2..5476a7cdff6 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -137,7 +137,7 @@ module IssuesHelper
end
def create_confidential_merge_request_enabled?
- Feature.enabled?(:create_confidential_merge_request, @project)
+ Feature.enabled?(:create_confidential_merge_request, @project, default_enabled: true)
end
def show_new_branch_button?
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 52bb797b5b3..8d3e54dc455 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -3,13 +3,14 @@
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
- value = can_create_confidential_merge_request? ? _('Create confidential merge request') : value
+ - create_mr_text = can_create_confidential_merge_request? ? _('Create confidential merge request') : _('Create merge request')
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
- create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
- .create-mr-dropdown-wrap.d-inline-block.full-width-mobile{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
+ .create-mr-dropdown-wrap.d-inline-block.full-width-mobile.js-create-mr{ data: { project_path: @project.full_path, project_id: @project.id, can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path, is_confidential: can_create_confidential_merge_request?.to_s } }
.btn-group.btn-group-sm.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin')
@@ -26,7 +27,7 @@
.droplab-dropdown
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-right.gl-show-field-errors{ class: ("create-confidential-merge-request-dropdown-menu" if can_create_confidential_merge_request?), data: { dropdown: true } }
- if can_create_merge_request
- %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } }
+ %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: create_mr_text } }
.menu-item
= icon('check', class: 'icon')
- if can_create_confidential_merge_request?
@@ -41,6 +42,8 @@
%li.divider.droplab-item-ignore
%li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16
+ - if can_create_confidential_merge_request?
+ #js-forked-project{ data: { namespace_path: @project.namespace.full_path, project_path: @project.full_path, new_fork_path: new_project_fork_path(@project), help_page_path: help_page_path('user/project/merge_requests') } }
.form-group
%label{ for: 'new-branch-name' }
= _('Branch name')
@@ -55,4 +58,8 @@
.form-group
%button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } }
- = _('Create merge request')
+ = create_mr_text
+
+ - if can_create_confidential_merge_request?
+ %p.text-warning.js-exposed-info-warning.hidden
+ = _('This may expose confidential information as the selected fork is in another namespace that can have other members.')
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8a4f57c5b13..05d291022dc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4169,6 +4169,9 @@ msgstr ""
msgid "Error fetching diverging counts for branches. Please try again."
msgstr ""
+msgid "Error fetching forked projects. Please try again."
+msgstr ""
+
msgid "Error fetching labels."
msgstr ""
@@ -6825,6 +6828,9 @@ msgstr ""
msgid "No files found."
msgstr ""
+msgid "No forks available to you."
+msgstr ""
+
msgid "No job trace"
msgstr ""
@@ -9192,6 +9198,9 @@ msgstr ""
msgid "Select members to invite"
msgstr ""
+msgid "Select private project"
+msgstr ""
+
msgid "Select project"
msgstr ""
@@ -10753,6 +10762,9 @@ msgstr ""
msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
msgstr ""
+msgid "This may expose confidential information as the selected fork is in another namespace that can have other members."
+msgstr ""
+
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr ""
@@ -11084,6 +11096,12 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
+msgid "To protect this issues confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgstr ""
+
+msgid "To protect this issues confidentiality, a private fork of this project was selected."
+msgstr ""
+
msgid "To see all the user's personal access tokens you must impersonate them first."
msgstr ""
diff --git a/spec/features/issues/user_creates_confidential_merge_request_spec.rb b/spec/features/issues/user_creates_confidential_merge_request_spec.rb
new file mode 100644
index 00000000000..7ae4af4667b
--- /dev/null
+++ b/spec/features/issues/user_creates_confidential_merge_request_spec.rb
@@ -0,0 +1,54 @@
+require 'rails_helper'
+
+describe 'User creates confidential merge request on issue page', :js do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, :public) }
+ let(:issue) { create(:issue, project: project, confidential: true) }
+
+ def visit_confidential_issue
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'user has no private fork' do
+ before do
+ fork_project(project, user, repository: true)
+ visit_confidential_issue
+ end
+
+ it 'shows that user has no fork available' do
+ click_button 'Create confidential merge request'
+
+ page.within '.create-confidential-merge-request-dropdown-menu' do
+ expect(page).to have_content('No forks available to you')
+ end
+ end
+ end
+
+ describe 'user has private fork' do
+ let(:forked_project) { fork_project(project, user, repository: true) }
+
+ before do
+ forked_project.update(visibility: Gitlab::VisibilityLevel::PRIVATE)
+ visit_confidential_issue
+ end
+
+ it 'create merge request in fork' do
+ click_button 'Create confidential merge request'
+
+ page.within '.create-confidential-merge-request-dropdown-menu' do
+ expect(page).to have_button(forked_project.name_with_namespace)
+ click_button 'Create confidential merge request'
+ end
+
+ expect(page).to have_content(forked_project.namespace.name)
+ end
+ end
+end
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
new file mode 100644
index 00000000000..a241c764df7
--- /dev/null
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Confidential merge request project form group component renders empty state when response is empty 1`] = `
+<div
+ class="form-group"
+>
+ <label>
+ Project
+ </label>
+
+ <div>
+ <!---->
+
+ <p
+ class="text-muted mt-1 mb-0"
+ >
+
+ No forks available to you.
+ <br />
+
+ <span>
+ To protect this issues confidentiality,
+ <a
+ class="help-link"
+ href="https://test.com"
+ >
+ fork the project
+ </a>
+ and set the forks visiblity to private.
+ </span>
+
+ <gllink-stub
+ class="help-link"
+ href="/help"
+ target="_blank"
+ >
+ <span
+ class="sr-only"
+ >
+ Read more
+ </span>
+
+ <i
+ aria-hidden="true"
+ class="fa fa-question-circle"
+ />
+ </gllink-stub>
+ </p>
+ </div>
+</div>
+`;
+
+exports[`Confidential merge request project form group component renders fork dropdown 1`] = `
+<div
+ class="form-group"
+>
+ <label>
+ Project
+ </label>
+
+ <div>
+ <!---->
+
+ <p
+ class="text-muted mt-1 mb-0"
+ >
+
+ No forks available to you.
+ <br />
+
+ <span>
+ To protect this issues confidentiality,
+ <a
+ class="help-link"
+ href="https://test.com"
+ >
+ fork the project
+ </a>
+ and set the forks visiblity to private.
+ </span>
+
+ <gllink-stub
+ class="help-link"
+ href="/help"
+ target="_blank"
+ >
+ <span
+ class="sr-only"
+ >
+ Read more
+ </span>
+
+ <i
+ aria-hidden="true"
+ class="fa fa-question-circle"
+ />
+ </gllink-stub>
+ </p>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/confidential_merge_request/components/dropdown_spec.js b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
new file mode 100644
index 00000000000..69495f3c161
--- /dev/null
+++ b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
@@ -0,0 +1,56 @@
+import { mount } from '@vue/test-utils';
+import { GlDropdownItem } from '@gitlab/ui';
+import Dropdown from '~/confidential_merge_request/components/dropdown.vue';
+
+let vm;
+
+function factory(projects = []) {
+ vm = mount(Dropdown, {
+ propsData: {
+ projects,
+ selectedProject: projects[0],
+ },
+ });
+}
+
+describe('Confidential merge request project dropdown component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it('renders dropdown items', () => {
+ factory([
+ {
+ id: 1,
+ name: 'test',
+ },
+ {
+ id: 2,
+ name: 'test',
+ },
+ ]);
+
+ expect(vm.findAll(GlDropdownItem).length).toBe(2);
+ });
+
+ it('renders selected project icon', () => {
+ factory([
+ {
+ id: 1,
+ name: 'test',
+ },
+ {
+ id: 2,
+ name: 'test 2',
+ },
+ ]);
+
+ expect(vm.find('.js-active-project-check').classes()).not.toContain('icon');
+ expect(
+ vm
+ .findAll('.js-active-project-check')
+ .at(1)
+ .classes(),
+ ).toContain('icon');
+ });
+});
diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
new file mode 100644
index 00000000000..3001363f7b9
--- /dev/null
+++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
@@ -0,0 +1,77 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
+
+const localVue = createLocalVue();
+const mockData = [
+ {
+ id: 1,
+ name_with_namespace: 'root / gitlab-ce',
+ path_with_namespace: 'root/gitlab-ce',
+ namespace: {
+ full_path: 'root',
+ },
+ },
+ {
+ id: 2,
+ name_with_namespace: 'test / gitlab-ce',
+ path_with_namespace: 'test/gitlab-ce',
+ namespace: {
+ full_path: 'test',
+ },
+ },
+];
+let vm;
+let mock;
+
+function factory(projects = mockData) {
+ mock = new MockAdapter(axios);
+ mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects);
+
+ vm = shallowMount(ProjectFormGroup, {
+ localVue,
+ propsData: {
+ namespacePath: 'gitlab-org',
+ projectPath: 'gitlab-org/gitlab-ce',
+ newForkPath: 'https://test.com',
+ helpPagePath: '/help',
+ },
+ });
+}
+
+describe('Confidential merge request project form group component', () => {
+ afterEach(() => {
+ mock.restore();
+ vm.destroy();
+ });
+
+ it('renders fork dropdown', () => {
+ factory();
+
+ return localVue.nextTick(() => {
+ expect(vm.element).toMatchSnapshot();
+ });
+ });
+
+ it('sets selected project as first fork', () => {
+ factory();
+
+ return localVue.nextTick(() => {
+ expect(vm.vm.selectedProject).toEqual({
+ id: 1,
+ name: 'root / gitlab-ce',
+ pathWithNamespace: 'root/gitlab-ce',
+ namespaceFullpath: 'root',
+ });
+ });
+ });
+
+ it('renders empty state when response is empty', () => {
+ factory([]);
+
+ return localVue.nextTick(() => {
+ expect(vm.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/javascripts/create_merge_request_dropdown_spec.js b/spec/frontend/create_merge_request_dropdown_spec.js
index 00fe3f451f5..6e41fdabdce 100644
--- a/spec/javascripts/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/create_merge_request_dropdown_spec.js
@@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
-import { TEST_HOST } from 'spec/test_constants';
+import { TEST_HOST } from './helpers/test_constants';
describe('CreateMergeRequestDropdown', () => {
let axiosMock;
@@ -10,7 +10,7 @@ describe('CreateMergeRequestDropdown', () => {
beforeEach(() => {
axiosMock = new MockAdapter(axios);
- setFixtures(`
+ document.body.innerHTML = `
<div id="dummy-wrapper-element">
<div class="available"></div>
<div class="unavailable">
@@ -18,11 +18,12 @@ describe('CreateMergeRequestDropdown', () => {
<div class="text"></div>
</div>
<div class="js-ref"></div>
+ <div class="js-create-mr"></div>
<div class="js-create-merge-request"></div>
<div class="js-create-target"></div>
<div class="js-dropdown-toggle"></div>
</div>
- `);
+ `;
const dummyElement = document.getElementById('dummy-wrapper-element');
dropdown = new CreateMergeRequestDropdown(dummyElement);
@@ -36,7 +37,7 @@ describe('CreateMergeRequestDropdown', () => {
describe('getRef', () => {
it('escapes branch names correctly', done => {
const endpoint = `${dropdown.refsPath}contains%23hash`;
- spyOn(axios, 'get').and.callThrough();
+ jest.spyOn(axios, 'get');
axiosMock.onGet(endpoint).replyOnce({});
dropdown