summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/google_api/authorizations_controller_spec.rb49
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb308
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb50
-rw-r--r--spec/factories/gcp/cluster.rb38
-rw-r--r--spec/features/issuables/discussion_lock_spec.rb106
-rw-r--r--spec/features/issues_spec.rb12
-rw-r--r--spec/features/merge_requests/discussion_lock_spec.rb49
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb32
-rw-r--r--spec/features/projects/clusters_spec.rb111
-rw-r--r--spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb19
-rw-r--r--spec/features/projects/merge_requests/user_closes_merge_request_spec.rb21
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_commit_spec.rb19
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_diff_spec.rb172
-rw-r--r--spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb50
-rw-r--r--spec/features/projects/merge_requests/user_creates_merge_request_spec.rb32
-rw-r--r--spec/features/projects/merge_requests/user_edits_merge_request_spec.rb25
-rw-r--r--spec/features/projects/merge_requests/user_manages_subscription_spec.rb32
-rw-r--r--spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb22
-rw-r--r--spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb63
-rw-r--r--spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb15
-rw-r--r--spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb15
-rw-r--r--spec/features/projects/merge_requests/user_views_diffs_spec.rb46
-rw-r--r--spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb15
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb92
-rw-r--r--spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb72
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json11
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issues.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json1
-rw-r--r--spec/javascripts/clusters_spec.js79
-rw-r--r--spec/javascripts/fixtures/clusters.rb34
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js16
-rw-r--r--spec/javascripts/repo/components/repo_sidebar_spec.js60
-rw-r--r--spec/javascripts/sidebar/lock/edit_form_buttons_spec.js36
-rw-r--r--spec/javascripts/sidebar/lock/edit_form_spec.js41
-rw-r--r--spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js71
-rw-r--r--spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js1
-rw-r--r--spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js20
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_warning_spec.js46
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml28
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb1
-rw-r--r--spec/lib/google_api/auth_spec.rb41
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb128
-rw-r--r--spec/lib/rspec_flaky/listener_spec.rb1
-rw-r--r--spec/models/gcp/cluster_spec.rb240
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/policies/gcp/cluster_policy_spec.rb28
-rw-r--r--spec/policies/issuable_policy_spec.rb28
-rw-r--r--spec/policies/note_policy_spec.rb71
-rw-r--r--spec/presenters/gcp/cluster_presenter_spec.rb35
-rw-r--r--spec/requests/api/notes_spec.rb34
-rw-r--r--spec/serializers/cluster_entity_spec.rb22
-rw-r--r--spec/serializers/cluster_serializer_spec.rb19
-rw-r--r--spec/services/ci/create_cluster_service_spec.rb47
-rw-r--r--spec/services/ci/fetch_gcp_operation_service_spec.rb36
-rw-r--r--spec/services/ci/fetch_kubernetes_token_service_spec.rb64
-rw-r--r--spec/services/ci/finalize_cluster_creation_service_spec.rb61
-rw-r--r--spec/services/ci/integrate_cluster_service_spec.rb42
-rw-r--r--spec/services/ci/provision_cluster_service_spec.rb85
-rw-r--r--spec/services/ci/update_cluster_service_spec.rb37
-rw-r--r--spec/services/issues/update_service_spec.rb12
-rw-r--r--spec/services/merge_requests/update_service_spec.rb11
-rw-r--r--spec/support/helpers/merge_request_diff_helpers.rb28
-rw-r--r--spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb28
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb23
-rw-r--r--spec/workers/concerns/cluster_queue_spec.rb15
-rw-r--r--spec/workers/wait_for_cluster_creation_worker_spec.rb67
67 files changed, 3047 insertions, 73 deletions
diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb
new file mode 100644
index 00000000000..80d553f0f34
--- /dev/null
+++ b/spec/controllers/google_api/authorizations_controller_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe GoogleApi::AuthorizationsController do
+ describe 'GET|POST #callback' do
+ let(:user) { create(:user) }
+ let(:token) { 'token' }
+ let(:expires_at) { 1.hour.since.strftime('%s') }
+
+ subject { get :callback, code: 'xxx', state: @state }
+
+ before do
+ sign_in(user)
+
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:get_token).and_return([token, expires_at])
+ end
+
+ it 'sets token and expires_at in session' do
+ subject
+
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token])
+ .to eq(token)
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at])
+ .to eq(expires_at)
+ end
+
+ context 'when redirect uri key is stored in state' do
+ set(:project) { create(:project) }
+ let(:redirect_uri) { project_clusters_url(project).to_s }
+
+ before do
+ @state = GoogleApi::CloudPlatform::Client
+ .new_session_key_for_redirect_uri do |key|
+ session[key] = redirect_uri
+ end
+ end
+
+ it 'redirects to the URL stored in state param' do
+ expect(subject).to redirect_to(redirect_uri)
+ end
+ end
+
+ context 'when redirection url is not stored in state' do
+ it 'redirects to root_path' do
+ expect(subject).to redirect_to(root_path)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
new file mode 100644
index 00000000000..7985028d73b
--- /dev/null
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -0,0 +1,308 @@
+require 'spec_helper'
+
+describe Projects::ClustersController do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ let(:role) { :master }
+
+ before do
+ project.team << [user, role]
+
+ sign_in(user)
+ end
+
+ describe 'GET index' do
+ subject do
+ get :index, namespace_id: project.namespace,
+ project_id: project
+ end
+
+ context 'when cluster is already created' do
+ let!(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ it 'redirects to show a cluster' do
+ subject
+
+ expect(response).to redirect_to(project_cluster_path(project, cluster))
+ end
+ end
+
+ context 'when we do not have cluster' do
+ it 'redirects to create a cluster' do
+ subject
+
+ expect(response).to redirect_to(new_project_cluster_path(project))
+ end
+ end
+ end
+
+ describe 'GET login' do
+ render_views
+
+ subject do
+ get :login, namespace_id: project.namespace,
+ project_id: project
+ end
+
+ context 'when we do have omniauth configured' do
+ it 'shows login button' do
+ subject
+
+ expect(response.body).to include('auth_buttons/signin_with_google')
+ end
+ end
+
+ context 'when we do not have omniauth configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'shows notice message' do
+ subject
+
+ expect(response.body).to include('Ask your GitLab administrator if you want to use this service.')
+ end
+ end
+ end
+
+ shared_examples 'requires to login' do
+ it 'redirects to create a cluster' do
+ subject
+
+ expect(response).to redirect_to(login_project_clusters_path(project))
+ end
+ end
+
+ describe 'GET new' do
+ render_views
+
+ subject do
+ get :new, namespace_id: project.namespace,
+ project_id: project
+ end
+
+ context 'when logged' do
+ before do
+ make_logged_in
+ end
+
+ it 'shows a creation form' do
+ subject
+
+ expect(response.body).to include('Create cluster')
+ end
+ end
+
+ context 'when not logged' do
+ it_behaves_like 'requires to login'
+ end
+ end
+
+ describe 'POST create' do
+ subject do
+ post :create, params.merge(namespace_id: project.namespace,
+ project_id: project)
+ end
+
+ context 'when not logged' do
+ let(:params) { {} }
+
+ it_behaves_like 'requires to login'
+ end
+
+ context 'when logged in' do
+ before do
+ make_logged_in
+ end
+
+ context 'when all required parameters are set' do
+ let(:params) do
+ {
+ cluster: {
+ gcp_cluster_name: 'new-cluster',
+ gcp_project_id: '111'
+ }
+ }
+ end
+
+ before do
+ expect(ClusterProvisionWorker).to receive(:perform_async) { }
+ end
+
+ it 'creates a new cluster' do
+ expect { subject }.to change { Gcp::Cluster.count }
+
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ end
+ end
+
+ context 'when not all required parameters are set' do
+ render_views
+
+ let(:params) do
+ {
+ cluster: {
+ project_namespace: 'some namespace'
+ }
+ }
+ end
+
+ it 'shows an error message' do
+ expect { subject }.not_to change { Gcp::Cluster.count }
+
+ expect(response).to render_template(:new)
+ end
+ end
+ end
+ end
+
+ describe 'GET status' do
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ subject do
+ get :status, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster,
+ format: :json
+ end
+
+ it "responds with matching schema" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+ end
+
+ describe 'GET show' do
+ render_views
+
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ subject do
+ get :show, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
+ end
+
+ context 'when logged as master' do
+ it "allows to update cluster" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Save")
+ end
+
+ it "allows remove integration" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response.body).to include("Remove integration")
+ end
+ end
+
+ context 'when logged as developer' do
+ let(:role) { :developer }
+
+ it "does not allow to access page" do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'PUT update' do
+ render_views
+
+ let(:service) { project.build_kubernetes_service }
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project, service: service) }
+ let(:params) { {} }
+
+ subject do
+ put :update, params.merge(namespace_id: project.namespace,
+ project_id: project,
+ id: cluster)
+ end
+
+ context 'when logged as master' do
+ context 'when valid params are used' do
+ let(:params) do
+ {
+ cluster: { enabled: false }
+ }
+ end
+
+ it "redirects back to show page" do
+ subject
+
+ expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when invalid params are used' do
+ let(:params) do
+ {
+ cluster: { project_namespace: 'my Namespace 321321321 #' }
+ }
+ end
+
+ it "rejects changes" do
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+ end
+ end
+
+ context 'when logged as developer' do
+ let(:role) { :developer }
+
+ it "does not allow to update cluster" do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'delete update' do
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, project: project) }
+
+ subject do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: cluster
+ end
+
+ context 'when logged as master' do
+ it "redirects back to clusters list" do
+ subject
+
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when logged as developer' do
+ let(:role) { :developer }
+
+ it "does not allow to destroy cluster" do
+ subject
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ def make_logged_in
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token] = '1234'
+ session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = in_hour.to_i.to_s
+ end
+
+ def in_hour
+ Time.now + 1.hour
+ end
+end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index c0337f96fc6..e3114e5c06e 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -266,6 +266,56 @@ describe Projects::NotesController do
end
end
end
+
+ context 'when the merge request discussion is locked' do
+ before do
+ project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ merge_request.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when a noteable is not found' do
+ it 'returns 404 status' do
+ request_params[:note][:noteable_id] = 9999
+ post :create, request_params.merge(format: :json)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when a user is a team member' do
+ it 'returns 302 status for html' do
+ post :create, request_params
+
+ expect(response).to have_http_status(302)
+ end
+
+ it 'returns 200 status for json' do
+ post :create, request_params.merge(format: :json)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'creates a new note' do
+ expect { post :create, request_params }.to change { Note.count }.by(1)
+ end
+ end
+
+ context 'when a user is not a team member' do
+ before do
+ project.project_member(user).destroy
+ end
+
+ it 'returns 404 status' do
+ post :create, request_params
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'does not create a new note' do
+ expect { post :create, request_params }.not_to change { Note.count }
+ end
+ end
+ end
end
describe 'DELETE destroy' do
diff --git a/spec/factories/gcp/cluster.rb b/spec/factories/gcp/cluster.rb
new file mode 100644
index 00000000000..630e40da888
--- /dev/null
+++ b/spec/factories/gcp/cluster.rb
@@ -0,0 +1,38 @@
+FactoryGirl.define do
+ factory :gcp_cluster, class: Gcp::Cluster do
+ project
+ user
+ enabled true
+ gcp_project_id 'gcp-project-12345'
+ gcp_cluster_name 'test-cluster'
+ gcp_cluster_zone 'us-central1-a'
+ gcp_cluster_size 1
+ gcp_machine_type 'n1-standard-4'
+
+ trait :with_kubernetes_service do
+ after(:create) do |cluster, evaluator|
+ create(:kubernetes_service, project: cluster.project).tap do |service|
+ cluster.update(service: service)
+ end
+ end
+ end
+
+ trait :custom_project_namespace do
+ project_namespace 'sample-app'
+ end
+
+ trait :created_on_gke do
+ status_event :make_created
+ endpoint '111.111.111.111'
+ ca_cert 'xxxxxx'
+ kubernetes_token 'xxxxxx'
+ username 'xxxxxx'
+ password 'xxxxxx'
+ end
+
+ trait :errored do
+ status_event :make_errored
+ status_reason 'general error'
+ end
+ end
+end
diff --git a/spec/features/issuables/discussion_lock_spec.rb b/spec/features/issuables/discussion_lock_spec.rb
new file mode 100644
index 00000000000..7ea29ff252b
--- /dev/null
+++ b/spec/features/issuables/discussion_lock_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe 'Discussion Lock', :js do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when a user is a team member' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when the discussion is unlocked' do
+ it 'the user can lock the issue' do
+ visit project_issue_path(project, issue)
+
+ expect(find('.issuable-sidebar')).to have_content('Unlocked')
+
+ page.within('.issuable-sidebar') do
+ find('.lock-edit').click
+ click_button('Lock')
+ end
+
+ expect(find('#notes')).to have_content('locked this issue')
+ end
+ end
+
+ context 'when the discussion is locked' do
+ before do
+ issue.update_attribute(:discussion_locked, true)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'the user can unlock the issue' do
+ expect(find('.issuable-sidebar')).to have_content('Locked')
+
+ page.within('.issuable-sidebar') do
+ find('.lock-edit').click
+ click_button('Unlock')
+ end
+
+ expect(find('#notes')).to have_content('unlocked this issue')
+ expect(find('.issuable-sidebar')).to have_content('Unlocked')
+ end
+
+ it 'the user can create a comment' do
+ page.within('#notes .js-main-target-form') do
+ fill_in 'note[note]', with: 'Some new comment'
+ click_button 'Comment'
+ end
+
+ wait_for_requests
+
+ expect(find('div#notes')).to have_content('Some new comment')
+ end
+ end
+ end
+
+ context 'when a user is not a team member' do
+ context 'when the discussion is unlocked' do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ it 'the user can not lock the issue' do
+ expect(find('.issuable-sidebar')).to have_content('Unlocked')
+ expect(find('.issuable-sidebar')).not_to have_selector('.lock-edit')
+ end
+
+ it 'the user can create a comment' do
+ page.within('#notes .js-main-target-form') do
+ fill_in 'note[note]', with: 'Some new comment'
+ click_button 'Comment'
+ end
+
+ wait_for_requests
+
+ expect(find('div#notes')).to have_content('Some new comment')
+ end
+ end
+
+ context 'when the discussion is locked' do
+ before do
+ issue.update_attribute(:discussion_locked, true)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'the user can not unlock the issue' do
+ expect(find('.issuable-sidebar')).to have_content('Locked')
+ expect(find('.issuable-sidebar')).not_to have_selector('.lock-edit')
+ end
+
+ it 'the user can not create a comment' do
+ page.within('#notes') do
+ expect(page).not_to have_selector('js-main-target-form')
+ expect(page.find('.disabled-comment'))
+ .to have_content('This issue is locked. Only project members can comment.')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index aa8cf3b013c..e2db1442d90 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -609,14 +609,14 @@ describe 'Issues', :js do
visit project_issue_path(project, issue)
- expect(page).to have_css('.confidential-issue-warning')
- expect(page).to have_css('.is-confidential')
- expect(page).not_to have_css('.is-not-confidential')
+ expect(page).to have_css('.issuable-note-warning')
+ expect(find('.issuable-sidebar-item.confidentiality')).to have_css('.is-active')
+ expect(find('.issuable-sidebar-item.confidentiality')).not_to have_css('.not-active')
find('.confidential-edit').click
- expect(page).to have_css('.confidential-warning-message')
+ expect(page).to have_css('.sidebar-item-warning-message')
- within('.confidential-warning-message') do
+ within('.sidebar-item-warning-message') do
find('.btn-close').click
end
@@ -624,7 +624,7 @@ describe 'Issues', :js do
visit project_issue_path(project, issue)
- expect(page).not_to have_css('.is-confidential')
+ expect(page).not_to have_css('.is-active')
end
end
end
diff --git a/spec/features/merge_requests/discussion_lock_spec.rb b/spec/features/merge_requests/discussion_lock_spec.rb
new file mode 100644
index 00000000000..7bbd3b1e69e
--- /dev/null
+++ b/spec/features/merge_requests/discussion_lock_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe 'Discussion Lock', :js do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: user) }
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when the discussion is locked' do
+ before do
+ merge_request.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when a user is a team member' do
+ before do
+ project.add_developer(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'the user can create a comment' do
+ page.within('.issuable-discussion #notes .js-main-target-form') do
+ fill_in 'note[note]', with: 'Some new comment'
+ click_button 'Comment'
+ end
+
+ wait_for_requests
+
+ expect(find('.issuable-discussion #notes')).to have_content('Some new comment')
+ end
+ end
+
+ context 'when a user is not a team member' do
+ before do
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'the user can not create a comment' do
+ page.within('.issuable-discussion #notes') do
+ expect(page).not_to have_selector('js-main-target-form')
+ expect(page.find('.disabled-comment'))
+ .to have_content('This merge request is locked. Only project members can comment.')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
index 2fb6d0b965f..e1ca4fe186c 100644
--- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Merge requests > User posts diff notes', :js do
+ include MergeRequestDiffHelpers
+
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.source_project }
@@ -244,36 +246,6 @@ feature 'Merge requests > User posts diff notes', :js do
expect(line[:num]).not_to have_css comment_button_class
end
- def get_line_components(line_holder, diff_side = nil)
- if diff_side.nil?
- get_inline_line_components(line_holder)
- else
- get_parallel_line_components(line_holder, diff_side)
- end
- end
-
- def get_inline_line_components(line_holder)
- { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
- end
-
- def get_parallel_line_components(line_holder, diff_side = nil)
- side_index = diff_side == 'left' ? 0 : 1
- # Wait for `.line_content`
- line_holder.find('.line_content', match: :first)
- # Wait for `.diff-line-num`
- line_holder.find('.diff-line-num', match: :first)
- { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
- end
-
- def click_diff_line(line_holder, diff_side = nil)
- line = get_line_components(line_holder, diff_side)
- line[:content].hover
-
- expect(line[:num]).to have_css comment_button_class
-
- line[:num].find(comment_button_class).trigger 'click'
- end
-
def write_comment_on_line(line_holder, diff_side)
click_diff_line(line_holder, diff_side)
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
new file mode 100644
index 00000000000..810f2c39b43
--- /dev/null
+++ b/spec/features/projects/clusters_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+feature 'Clusters', :js do
+ let!(:project) { create(:project, :repository) }
+ let!(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ gitlab_sign_in(user)
+ end
+
+ context 'when user has signed in Google' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:validate_token).and_return(true)
+ end
+
+ context 'when user does not have a cluster and visits cluster index page' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees a new page' do
+ expect(page).to have_button('Create cluster')
+ end
+
+ context 'when user filled form with valid parameters' do
+ before do
+ double.tap do |dbl|
+ allow(dbl).to receive(:status).and_return('RUNNING')
+ allow(dbl).to receive(:self_link)
+ .and_return('projects/gcp-project-12345/zones/us-central1-a/operations/ope-123')
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create).and_return(dbl)
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+
+ fill_in 'cluster_gcp_project_id', with: 'gcp-project-123'
+ fill_in 'cluster_gcp_cluster_name', with: 'dev-cluster'
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a cluster details page and creation status' do
+ expect(page).to have_content('Cluster is being created on Google Container Engine...')
+
+ Gcp::Cluster.last.make_created!
+
+ expect(page).to have_content('Cluster was successfully created on Google Container Engine')
+ end
+ end
+
+ context 'when user filled form with invalid parameters' do
+ before do
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
+ end
+ end
+ end
+
+ context 'when user has a cluster and visits cluster index page' do
+ let!(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service, project: project) }
+
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees an cluster details page' do
+ expect(page).to have_button('Save')
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.gcp_cluster_name)
+ end
+
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-toggle-cluster').click
+ click_button 'Save'
+ end
+
+ it 'user sees the succeccful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when user destory the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the succeccful message' do
+ expect(page).to have_content('Cluster integration was successfully removed.')
+ expect(page).to have_button('Create cluster')
+ end
+ end
+ end
+ end
+
+ context 'when user has not signed in Google' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees a login page' do
+ expect(page).to have_css('.signin-with-google')
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
index 6c0b5e279d5..c35ba2d7016 100644
--- a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
+++ b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb
@@ -62,4 +62,23 @@ describe 'User accepts a merge request', :js do
wait_for_requests
end
end
+
+ context 'when modifying the merge commit message' do
+ before do
+ merge_request.mark_as_mergeable
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'accepts a merge request' do
+ click_button('Modify commit message')
+ fill_in('Commit message', with: 'wow such merge')
+
+ click_button('Merge')
+
+ page.within('.status-box') do
+ expect(page).to have_content('Merged')
+ end
+ end
+ end
end
diff --git a/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb b/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb
new file mode 100644
index 00000000000..b257f447439
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_closes_merge_request_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'User closes a merge requests', :js do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'closes a merge request' do
+ click_link('Close merge request', match: :first)
+
+ expect(page).to have_content(merge_request.title)
+ expect(page).to have_content('Closed by')
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb b/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb
new file mode 100644
index 00000000000..0a952cfc2a9
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_comments_on_commit_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe 'User comments on a commit', :js do
+ include MergeRequestDiffHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+ end
+
+ include_examples 'comment on merge request file'
+end
diff --git a/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
new file mode 100644
index 00000000000..f34302f25f8
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_comments_on_diff_spec.rb
@@ -0,0 +1,172 @@
+require 'spec_helper'
+
+describe 'User comments on a diff', :js do
+ include MergeRequestDiffHelpers
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
+ end
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(diffs_project_merge_request_path(project, merge_request))
+ end
+
+ context 'when viewing comments' do
+ context 'when toggling inline comments' do
+ context 'in a single file' do
+ it 'hides a comment' do
+ click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ page.within('.files > div:nth-child(3)') do
+ expect(page).to have_content('Line is wrong')
+
+ find('.js-toggle-diff-comments').trigger('click')
+
+ expect(page).not_to have_content('Line is wrong')
+ end
+ end
+ end
+
+ context 'in multiple files' do
+ it 'toggles comments' do
+ click_diff_line(find("[id='#{sample_compare.changes[0][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: 'Line is correct')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.files > div:nth-child(2) .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+
+ click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ # Hide the comment.
+ page.within('.files > div:nth-child(3)') do
+ find('.js-toggle-diff-comments').trigger('click')
+
+ expect(page).not_to have_content('Line is wrong')
+ end
+
+ # At this moment a user should see only one comment.
+ # The other one should be hidden.
+ page.within('.files > div:nth-child(2) .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+
+ # Show the comment.
+ page.within('.files > div:nth-child(3)') do
+ find('.js-toggle-diff-comments').trigger('click')
+ end
+
+ # Now both the comments should be shown.
+ page.within('.files > div:nth-child(3) .note-body > .note-text') do
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.files > div:nth-child(2) .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+
+ # Check the same comments in the side-by-side view.
+ click_link('Side-by-side')
+
+ wait_for_requests
+
+ page.within('.files > div:nth-child(3) .parallel .note-body > .note-text') do
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.files > div:nth-child(2) .parallel .note-body > .note-text') do
+ expect(page).to have_content('Line is correct')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when adding comments' do
+ include_examples 'comment on merge request file'
+ end
+
+ context 'when editing comments' do
+ it 'edits a comment' do
+ click_diff_line(find("[id='#{sample_commit.line_code}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in(:note_note, with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ page.within('.diff-file:nth-of-type(5) .note') do
+ find('.js-note-edit').click
+
+ page.within('.current-note-edit-form') do
+ fill_in('note_note', with: 'Typo, please fix')
+ click_button('Save comment')
+ end
+
+ expect(page).not_to have_button('Save comment', disabled: true)
+ end
+
+ page.within('.diff-file:nth-of-type(5) .note') do
+ expect(page).to have_content('Typo, please fix').and have_no_content('Line is wrong')
+ end
+ end
+ end
+
+ context 'when deleting comments' do
+ it 'deletes a comment' do
+ click_diff_line(find("[id='#{sample_commit.line_code}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in(:note_note, with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('1')
+ end
+
+ page.within('.diff-file:nth-of-type(5) .note') do
+ find('.more-actions').click
+ find('.more-actions .dropdown-menu li', match: :first)
+
+ find('.js-note-delete').click
+ end
+
+ page.within('.merge-request-tabs') do
+ find('.notes-tab').trigger('click')
+ end
+
+ wait_for_requests
+
+ expect(page).not_to have_css('.notes .discussion')
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('0')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb b/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb
new file mode 100644
index 00000000000..2eb652147ce
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_comments_on_merge_request_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe 'User comments on a merge request', :js do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'adds a comment' do
+ page.within('.js-main-target-form') do
+ fill_in(:note_note, with: '# Comment with a header')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.note') do
+ expect(page).to have_content('Comment with a header')
+ expect(page).not_to have_css('#comment-with-a-header')
+ end
+ end
+
+ it 'loads new comment' do
+ # Add new comment in background in order to check
+ # if it's going to be loaded automatically for current user.
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request, author: user, note: 'Line is wrong')
+
+ # Trigger a refresh of notes.
+ execute_script("$(document).trigger('visibilitychange');")
+ wait_for_requests
+
+ page.within('.notes .discussion') do
+ expect(page).to have_content("#{user.name} #{user.to_reference} started a discussion")
+ expect(page).to have_content(sample_commit.line_code_path)
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('1')
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
new file mode 100644
index 00000000000..f285c6c8783
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_creates_merge_request_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'User creates a merge request', :js do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_new_merge_request_path(project))
+ end
+
+ it 'creates a merge request' do
+ find('.js-source-branch').click
+ click_link('fix')
+
+ find('.js-target-branch').click
+ click_link('feature')
+
+ click_button('Compare branches')
+
+ fill_in('merge_request_title', with: 'Wiki Feature')
+ click_button('Submit merge request')
+
+ page.within('.merge-request') do
+ expect(page).to have_content('Wiki Feature')
+ end
+
+ wait_for_requests
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
new file mode 100644
index 00000000000..f6e3997383f
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_edits_merge_request_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'User edits a merge request', :js do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(edit_project_merge_request_path(project, merge_request))
+ end
+
+ it 'changes the target branch' do
+ expect(page).to have_content('Target branch')
+
+ first('.target_branch').click
+ select('merge-test', from: 'merge_request_target_branch', visible: false)
+ click_button('Save changes')
+
+ expect(page).to have_content("Request to merge #{merge_request.source_branch} into merge-test")
+ expect(page).to have_content("changed target branch from #{merge_request.target_branch} to merge-test")
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_manages_subscription_spec.rb b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
new file mode 100644
index 00000000000..30a80f8e652
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_manages_subscription_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'User manages subscription', :js do
+ let(:project) { create(:project, :public, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'toggles subscription' do
+ subscribe_button = find('.issuable-subscribe-button span')
+
+ expect(subscribe_button).to have_content('Subscribe')
+
+ click_on('Subscribe')
+
+ wait_for_requests
+
+ expect(subscribe_button).to have_content('Unsubscribe')
+
+ click_on('Unsubscribe')
+
+ wait_for_requests
+
+ expect(subscribe_button).to have_content('Subscribe')
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb
new file mode 100644
index 00000000000..ba3c9789da1
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_reopens_merge_request_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe 'User reopens a merge requests', :js do
+ let(:project) { create(:project, :public, :repository) }
+ let!(:merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'reopens a merge request' do
+ click_link('Reopen merge request', match: :first)
+
+ page.within('.status-box') do
+ expect(page).to have_content('Open')
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
new file mode 100644
index 00000000000..d8d9f7e2a8c
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_sorts_merge_requests_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe 'User sorts merge requests' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:merge_request2) do
+ create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
+ end
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'keeps the sort option' do
+ find('button.dropdown-toggle').click
+
+ page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ click_link('Last updated')
+ end
+
+ visit(merge_requests_dashboard_path(assignee_id: user.id))
+
+ expect(find('.issues-filters')).to have_content('Last updated')
+
+ visit(project_merge_requests_path(project))
+
+ expect(find('.issues-filters')).to have_content('Last updated')
+ end
+
+ context 'when merge requests have awards' do
+ before do
+ create_list(:award_emoji, 2, awardable: merge_request)
+ create(:award_emoji, :downvote, awardable: merge_request)
+
+ create(:award_emoji, awardable: merge_request2)
+ create_list(:award_emoji, 2, :downvote, awardable: merge_request2)
+ end
+
+ it 'sorts by popularity' do
+ find('button.dropdown-toggle').click
+
+ page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+ click_link('Popularity')
+ end
+
+ page.within('.mr-list') do
+ page.within('li.merge-request:nth-child(1)') do
+ expect(page).to have_content(merge_request.title)
+ expect(page).to have_content('2 1')
+ end
+
+ page.within('li.merge-request:nth-child(2)') do
+ expect(page).to have_content(merge_request2.title)
+ expect(page).to have_content('1 2')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb
new file mode 100644
index 00000000000..6c695bd7aa9
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_all_merge_requests_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User views all merge requests' do
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(project_merge_requests_path(project, state: :all))
+ end
+
+ it 'shows all merge requests' do
+ expect(page).to have_content(merge_request.title).and have_content(closed_merge_request.title)
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb
new file mode 100644
index 00000000000..853809fe87a
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_closed_merge_requests_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User views closed merge requests' do
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(project_merge_requests_path(project, state: :closed))
+ end
+
+ it 'shows closed merge requests' do
+ expect(page).to have_content(closed_merge_request.title).and have_no_content(merge_request.title)
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_diffs_spec.rb b/spec/features/projects/merge_requests/user_views_diffs_spec.rb
new file mode 100644
index 00000000000..295eb02b625
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_diffs_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe 'User views diffs', :js do
+ let(:merge_request) do
+ create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
+ end
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ visit(diffs_project_merge_request_path(project, merge_request))
+
+ wait_for_requests
+ end
+
+ shared_examples 'unfold diffs' do
+ it 'unfolds diffs' do
+ first('.js-unfold').click
+
+ expect(first('.text-file')).to have_content('.bundle')
+ end
+ end
+
+ it 'shows diffs' do
+ expect(page).to have_css('.tab-content #diffs.active')
+ expect(page).to have_css('#parallel-diff-btn', count: 1)
+ expect(page).to have_css('#inline-diff-btn', count: 1)
+ end
+
+ context 'when in the inline view' do
+ include_examples 'unfold diffs'
+ end
+
+ context 'when in the side-by-side view' do
+ before do
+ click_link('Side-by-side')
+
+ wait_for_requests
+ end
+
+ it 'shows diffs in parallel' do
+ expect(page).to have_css('.parallel')
+ end
+
+ include_examples 'unfold diffs'
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb
new file mode 100644
index 00000000000..eb012694f1e
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_merged_merge_requests_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User views merged merge requests' do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:merged_merge_request) { create(:merged_merge_request, source_project: project, target_project: project) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(project_merge_requests_path(project, state: :merged))
+ end
+
+ it 'shows merged merge requests' do
+ expect(page).to have_content(merged_merge_request.title).and have_no_content(merge_request.title)
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
new file mode 100644
index 00000000000..8970cf54457
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_open_merge_request_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe 'User views an open merge request' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project, description: '# Description header')
+ end
+
+ context 'when a merge request does not have repository' do
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'renders both the title and the description' do
+ node = find('.wiki h1 a#user-content-description-header')
+ expect(node[:href]).to end_with('#description-header')
+
+ # Work around a weird Capybara behavior where calling `parent` on a node
+ # returns the whole document, not the node's actual parent element
+ expect(find(:xpath, "#{node.path}/..").text).to eq(merge_request.description[2..-1])
+
+ expect(page).to have_content(merge_request.title).and have_content(merge_request.description)
+ end
+ end
+
+ context 'when a merge request has repository', :js do
+ let(:project) { create(:project, :public, :repository) }
+
+ context 'when rendering description preview' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(edit_project_merge_request_path(project, merge_request))
+ end
+
+ it 'renders empty description preview' do
+ find('.gfm-form').fill_in(:merge_request_description, with: '')
+
+ page.within('.gfm-form') do
+ click_link('Preview')
+
+ expect(find('.js-md-preview')).to have_content('Nothing to preview.')
+ end
+ end
+
+ it 'renders description preview' do
+ find('.gfm-form').fill_in(:merge_request_description, with: ':+1: Nice')
+
+ page.within('.gfm-form') do
+ click_link('Preview')
+
+ expect(find('.js-md-preview')).to have_css('gl-emoji')
+ end
+
+ expect(find('.gfm-form')).to have_css('.js-md-preview').and have_link('Write')
+ expect(find('#merge_request_description', visible: false)).not_to be_visible
+ end
+ end
+
+ context 'when the branch is rebased on the target' do
+ let(:merge_request) { create(:merge_request, :rebased, source_project: project, target_project: project) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'does not show diverged commits count' do
+ page.within('.mr-source-target') do
+ expect(page).not_to have_content(/([0-9]+ commit[s]? behind)/)
+ end
+ end
+ end
+
+ context 'when the branch is diverged on the target' do
+ let(:merge_request) { create(:merge_request, :diverged, source_project: project, target_project: project) }
+
+ before do
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'shows diverged commits count' do
+ page.within('.mr-source-target') do
+ expect(page).to have_content(/([0-9]+ commits behind)/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
new file mode 100644
index 00000000000..07b8c1ef479
--- /dev/null
+++ b/spec/features/projects/merge_requests/user_views_open_merge_requests_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe 'User views open merge requests' do
+ let(:project) { create(:project, :public, :repository) }
+
+ context "when the target branch is the project's default branch" do
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
+
+ before do
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows open merge requests' do
+ expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
+ end
+
+ it 'does not show target branch name' do
+ expect(page).to have_content(merge_request.title)
+ expect(find('.issuable-info')).not_to have_content(project.default_branch)
+ end
+ end
+
+ context "when the target branch is different from the project's default branch" do
+ let!(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'feature_conflict')
+ end
+
+ before do
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows target branch name' do
+ expect(page).to have_content(merge_request.target_branch)
+ end
+ end
+
+ context 'when a merge request has pipelines' do
+ let!(:build) { create :ci_build, pipeline: pipeline }
+
+ let(:merge_request) do
+ create(:merge_request_with_diffs,
+ source_project: project,
+ target_project: project,
+ source_branch: 'merge-test')
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ head_pipeline_of: merge_request)
+ end
+
+ before do
+ project.enable_ci
+
+ visit(project_merge_requests_path(project))
+ end
+
+ it 'shows pipeline status' do
+ page.within('.mr-list') do
+ expect(page).to have_link('Pipeline: pending')
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
new file mode 100644
index 00000000000..1f255a17881
--- /dev/null
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "required" : [
+ "status"
+ ],
+ "properties" : {
+ "status": { "type": "string" },
+ "status_reason": { "type": ["string", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json
index 03c422ab023..5c08dbc3b96 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issues.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issues.json
@@ -9,6 +9,7 @@
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
+ "discussion_locked": { "type": ["boolean", "null"] },
"closed_at": { "type": "date" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
index 31b3f4ba946..5828be5255b 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -72,6 +72,7 @@
"user_notes_count": { "type": "integer" },
"should_remove_source_branch": { "type": ["boolean", "null"] },
"force_remove_source_branch": { "type": ["boolean", "null"] },
+ "discussion_locked": { "type": ["boolean", "null"] },
"web_url": { "type": "uri" },
"time_stats": {
"time_estimate": { "type": "integer" },
diff --git a/spec/javascripts/clusters_spec.js b/spec/javascripts/clusters_spec.js
new file mode 100644
index 00000000000..eb1cd6eb804
--- /dev/null
+++ b/spec/javascripts/clusters_spec.js
@@ -0,0 +1,79 @@
+import Clusters from '~/clusters';
+
+describe('Clusters', () => {
+ let cluster;
+ preloadFixtures('clusters/show_cluster.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('clusters/show_cluster.html.raw');
+ cluster = new Clusters();
+ });
+
+ describe('toggle', () => {
+ it('should update the button and the input field on click', () => {
+ cluster.toggleButton.click();
+
+ expect(
+ cluster.toggleButton.classList,
+ ).not.toContain('checked');
+
+ expect(
+ cluster.toggleInput.getAttribute('value'),
+ ).toEqual('false');
+ });
+ });
+
+ describe('updateContainer', () => {
+ describe('when creating cluster', () => {
+ it('should show the creating container', () => {
+ cluster.updateContainer('creating');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('when cluster is created', () => {
+ it('should show the success container', () => {
+ cluster.updateContainer('created');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('when cluster has error', () => {
+ it('should show the error container', () => {
+ cluster.updateContainer('errored', 'this is an error');
+
+ expect(
+ cluster.creatingContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.successContainer.classList.contains('hidden'),
+ ).toBeTruthy();
+ expect(
+ cluster.errorContainer.classList.contains('hidden'),
+ ).toBeFalsy();
+
+ expect(
+ cluster.errorReasonContainer.textContent,
+ ).toContain('this is an error');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
new file mode 100644
index 00000000000..5774f36f026
--- /dev/null
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace) }
+ let(:cluster) { project.create_cluster!(gcp_cluster_name: "gke-test-creation-1", gcp_project_id: 'gitlab-internal-153318', gcp_cluster_zone: 'us-central1-a', gcp_cluster_size: '1', project_namespace: 'aaa', gcp_machine_type: 'n1-standard-1')}
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('clusters/')
+ end
+
+ before do
+ sign_in(admin)
+ end
+
+ after do
+ remove_repository(project)
+ end
+
+ it 'clusters/show_cluster.html.raw' do |example|
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: cluster
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
index f15633bd8b9..620b604f404 100644
--- a/spec/javascripts/repo/components/repo_file_spec.js
+++ b/spec/javascripts/repo/components/repo_file_spec.js
@@ -29,15 +29,17 @@ describe('RepoFile', () => {
}).$mount();
}
- beforeEach(() => {
- spyOn(repoFile.mixins[0].methods, 'timeFormated').and.returnValue(updated);
- });
-
it('renders link, icon, name and last commit details', () => {
- const vm = createComponent({
- file,
- activeFile,
+ const RepoFile = Vue.extend(repoFile);
+ const vm = new RepoFile({
+ propsData: {
+ file,
+ activeFile,
+ },
});
+ spyOn(vm, 'timeFormated').and.returnValue(updated);
+ vm.$mount();
+
const name = vm.$el.querySelector('.repo-file-name');
const fileIcon = vm.$el.querySelector('.file-icon');
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js
index 23c10ea022e..35d2b37ac2a 100644
--- a/spec/javascripts/repo/components/repo_sidebar_spec.js
+++ b/spec/javascripts/repo/components/repo_sidebar_spec.js
@@ -5,18 +5,26 @@ import RepoStore from '~/repo/stores/repo_store';
import repoSidebar from '~/repo/components/repo_sidebar.vue';
describe('RepoSidebar', () => {
+ let vm;
+
function createComponent() {
const RepoSidebar = Vue.extend(repoSidebar);
return new RepoSidebar().$mount();
}
+ afterEach(() => {
+ vm.$destroy();
+ });
+
it('renders a sidebar', () => {
RepoStore.files = [{
id: 0,
}];
RepoStore.openedFiles = [];
- const vm = createComponent();
+ RepoStore.isRoot = false;
+
+ vm = createComponent();
const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody');
@@ -35,7 +43,7 @@ describe('RepoSidebar', () => {
RepoStore.openedFiles = [{
id: 0,
}];
- const vm = createComponent();
+ vm = createComponent();
expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
expect(vm.$el.querySelector('thead')).toBeFalsy();
@@ -47,7 +55,7 @@ describe('RepoSidebar', () => {
tree: true,
};
RepoStore.files = [];
- const vm = createComponent();
+ vm = createComponent();
expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
});
@@ -57,7 +65,7 @@ describe('RepoSidebar', () => {
id: 0,
}];
RepoStore.isRoot = true;
- const vm = createComponent();
+ vm = createComponent();
expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
});
@@ -72,7 +80,7 @@ describe('RepoSidebar', () => {
};
RepoStore.files = [file1];
RepoStore.isRoot = true;
- const vm = createComponent();
+ vm = createComponent();
vm.fileClicked(file1);
@@ -87,7 +95,7 @@ describe('RepoSidebar', () => {
spyOn(Helper, 'getFileFromPath').and.returnValue(file);
spyOn(RepoStore, 'setActiveFiles');
- const vm = createComponent();
+ vm = createComponent();
vm.fileClicked(file);
expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file);
@@ -103,7 +111,7 @@ describe('RepoSidebar', () => {
};
RepoStore.files = [file1];
RepoStore.isRoot = true;
- const vm = createComponent();
+ vm = createComponent();
vm.fileClicked(file1);
@@ -114,12 +122,48 @@ describe('RepoSidebar', () => {
describe('goToPreviousDirectoryClicked', () => {
it('should hide files in directory if already open', () => {
const prevUrl = 'foo/bar';
- const vm = createComponent();
+ vm = createComponent();
vm.goToPreviousDirectoryClicked(prevUrl);
expect(RepoService.url).toEqual(prevUrl);
});
});
+
+ describe('back button', () => {
+ const file1 = {
+ id: 1,
+ url: 'file1',
+ };
+ const file2 = {
+ id: 2,
+ url: 'file2',
+ };
+ RepoStore.files = [file1, file2];
+ RepoStore.openedFiles = [file1, file2];
+ RepoStore.isRoot = true;
+
+ vm = createComponent();
+ vm.fileClicked(file1);
+
+ it('render previous file when using back button', () => {
+ spyOn(Helper, 'getContent').and.callThrough();
+
+ vm.fileClicked(file2);
+ expect(Helper.getContent).toHaveBeenCalledWith(file2);
+ Helper.getContent.calls.reset();
+
+ history.pushState({
+ key: Math.random(),
+ }, '', file1.url);
+ const popEvent = document.createEvent('Event');
+ popEvent.initEvent('popstate', true, true);
+ window.dispatchEvent(popEvent);
+
+ expect(Helper.getContent.calls.mostRecent().args[0].url).toContain(file1.url);
+
+ window.history.pushState({}, null, '/');
+ });
+ });
});
});
diff --git a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
new file mode 100644
index 00000000000..b0ea8ae0206
--- /dev/null
+++ b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('EditFormButtons', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(editFormButtons);
+ const toggleForm = () => { };
+ const updateLockedAttribute = () => { };
+
+ vm1 = mountComponent(Component, {
+ isLocked: true,
+ toggleForm,
+ updateLockedAttribute,
+ });
+
+ vm2 = mountComponent(Component, {
+ isLocked: false,
+ toggleForm,
+ updateLockedAttribute,
+ });
+ });
+
+ it('renders unlock or lock text based on locked state', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Unlock'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('Lock'),
+ ).toBe(true);
+ });
+});
diff --git a/spec/javascripts/sidebar/lock/edit_form_spec.js b/spec/javascripts/sidebar/lock/edit_form_spec.js
new file mode 100644
index 00000000000..7abd6997a18
--- /dev/null
+++ b/spec/javascripts/sidebar/lock/edit_form_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import editForm from '~/sidebar/components/lock/edit_form.vue';
+
+describe('EditForm', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(editForm);
+ const toggleForm = () => { };
+ const updateLockedAttribute = () => { };
+
+ vm1 = new Component({
+ propsData: {
+ isLocked: true,
+ toggleForm,
+ updateLockedAttribute,
+ issuableType: 'issue',
+ },
+ }).$mount();
+
+ vm2 = new Component({
+ propsData: {
+ isLocked: false,
+ toggleForm,
+ updateLockedAttribute,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+ });
+
+ it('renders on the appropriate warning text', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Unlock this issue?'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('Lock this merge request?'),
+ ).toBe(true);
+ });
+});
diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
new file mode 100644
index 00000000000..696fca516bc
--- /dev/null
+++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
@@ -0,0 +1,71 @@
+import Vue from 'vue';
+import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
+
+describe('LockIssueSidebar', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(lockIssueSidebar);
+
+ const mediator = {
+ service: {
+ update: Promise.resolve(true),
+ },
+
+ store: {
+ isLockDialogOpen: false,
+ },
+ };
+
+ vm1 = new Component({
+ propsData: {
+ isLocked: true,
+ isEditable: true,
+ mediator,
+ issuableType: 'issue',
+ },
+ }).$mount();
+
+ vm2 = new Component({
+ propsData: {
+ isLocked: false,
+ isEditable: false,
+ mediator,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+ });
+
+ it('shows if locked and/or editable', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Edit'),
+ ).toBe(true);
+
+ expect(
+ vm1.$el.innerHTML.includes('Locked'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('Unlocked'),
+ ).toBe(true);
+ });
+
+ it('displays the edit form when editable', (done) => {
+ expect(vm1.isLockDialogOpen).toBe(false);
+
+ vm1.$el.querySelector('.lock-edit').click();
+
+ expect(vm1.isLockDialogOpen).toBe(true);
+
+ vm1.$nextTick(() => {
+ expect(
+ vm1.$el
+ .innerHTML
+ .includes('Unlock this issue?'),
+ ).toBe(true);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
index b63633c03b8..e667b4b3677 100644
--- a/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
+++ b/spec/javascripts/vue_mr_widget/services/mr_widget_service_spec.js
@@ -31,6 +31,7 @@ describe('MRWidgetService', () => {
});
it('should have methods defined', () => {
+ window.history.pushState({}, null, '/');
const service = new MRWidgetService(mr);
expect(service.merge()).toBeDefined();
diff --git a/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js
deleted file mode 100644
index 6df08f3ebe7..00000000000
--- a/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import Vue from 'vue';
-import confidentialIssue from '~/vue_shared/components/issue/confidential_issue_warning.vue';
-
-describe('Confidential Issue Warning Component', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(confidentialIssue);
- vm = new Component().$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render confidential issue warning information', () => {
- expect(vm.$el.querySelector('i').className).toEqual('fa fa-eye-slash');
- expect(vm.$el.querySelector('span').textContent.trim()).toEqual('This is a confidential issue. Your comment will not be visible to the public.');
- });
-});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
new file mode 100644
index 00000000000..2cf4d8e00ed
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+const IssueWarning = Vue.extend(issueWarning);
+
+function formatWarning(string) {
+ // Replace newlines with a space then replace multiple spaces with one space
+ return string.trim().replace(/\n/g, ' ').replace(/\s\s+/g, ' ');
+}
+
+describe('Issue Warning Component', () => {
+ describe('isLocked', () => {
+ it('should render locked issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isLocked: true,
+ });
+
+ expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-lock');
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is locked. Only project members can comment.');
+ });
+ });
+
+ describe('isConfidential', () => {
+ it('should render confidential issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isConfidential: true,
+ });
+
+ expect(vm.$el.querySelector('i').className).toEqual('fa icon fa-eye-slash');
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This is a confidential issue. Your comment will not be visible to the public.');
+ });
+ });
+
+ describe('isLocked and isConfidential', () => {
+ it('should render locked and confidential issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isLocked: true,
+ isConfidential: true,
+ });
+
+ expect(vm.$el.querySelector('i')).toBeFalsy();
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual('This issue is confidential and locked. People without permission will never get a notification and won\'t be able to comment.');
+ });
+ });
+});
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 3fb8edeb701..ec425fd2803 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -147,6 +147,10 @@ deploy_keys:
- user
- deploy_keys_projects
- projects
+cluster:
+- project
+- user
+- service
services:
- project
- service_hook
@@ -177,6 +181,7 @@ project:
- tag_taggings
- tags
- chat_services
+- cluster
- creator
- group
- namespace
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 48938346577..80d92b2e6a3 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -25,6 +25,7 @@ Issue:
- relative_position
- last_edited_at
- last_edited_by_id
+- discussion_locked
Event:
- id
- target_type
@@ -168,6 +169,7 @@ MergeRequest:
- last_edited_at
- last_edited_by_id
- head_pipeline_id
+- discussion_locked
MergeRequestDiff:
- id
- state
@@ -311,6 +313,32 @@ Ci::PipelineSchedule:
- deleted_at
- created_at
- updated_at
+Gcp::Cluster:
+- id
+- project_id
+- user_id
+- service_id
+- enabled
+- status
+- status_reason
+- project_namespace
+- endpoint
+- ca_cert
+- encrypted_kubernetes_token
+- encrypted_kubernetes_token_iv
+- username
+- encrypted_password
+- encrypted_password_iv
+- gcp_project_id
+- gcp_cluster_zone
+- gcp_cluster_name
+- gcp_cluster_size
+- gcp_machine_type
+- gcp_operation_id
+- encrypted_gcp_token
+- encrypted_gcp_token_iv
+- created_at
+- updated_at
DeployKey:
- id
- user_id
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index ee152872acc..777e9c8e21d 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -60,6 +60,7 @@ describe Gitlab::UsageData do
deploy_keys
deployments
environments
+ gcp_clusters
in_review_folder
groups
issues
diff --git a/spec/lib/google_api/auth_spec.rb b/spec/lib/google_api/auth_spec.rb
new file mode 100644
index 00000000000..87a3f43274f
--- /dev/null
+++ b/spec/lib/google_api/auth_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe GoogleApi::Auth do
+ let(:redirect_uri) { 'http://localhost:3000/google_api/authorizations/callback' }
+ let(:redirect_to) { 'http://localhost:3000/namaspace/project/clusters' }
+
+ let(:client) do
+ GoogleApi::CloudPlatform::Client
+ .new(nil, redirect_uri, state: redirect_to)
+ end
+
+ describe '#authorize_url' do
+ subject { client.authorize_url }
+
+ it 'returns authorize_url' do
+ is_expected.to start_with('https://accounts.google.com/o/oauth2')
+ is_expected.to include(URI.encode(redirect_uri, URI::PATTERN::RESERVED))
+ is_expected.to include(URI.encode(redirect_to, URI::PATTERN::RESERVED))
+ end
+ end
+
+ describe '#get_token' do
+ let(:token) do
+ double.tap do |dbl|
+ allow(dbl).to receive(:token).and_return('token')
+ allow(dbl).to receive(:expires_at).and_return('expires_at')
+ end
+ end
+
+ before do
+ allow_any_instance_of(OAuth2::Strategy::AuthCode)
+ .to receive(:get_token).and_return(token)
+ end
+
+ it 'returns token and expires_at' do
+ token, expires_at = client.get_token('xxx')
+ expect(token).to eq('token')
+ expect(expires_at).to eq('expires_at')
+ end
+ end
+end
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
new file mode 100644
index 00000000000..acc5bd1da35
--- /dev/null
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -0,0 +1,128 @@
+require 'spec_helper'
+
+describe GoogleApi::CloudPlatform::Client do
+ let(:token) { 'token' }
+ let(:client) { described_class.new(token, nil) }
+
+ describe '.session_key_for_redirect_uri' do
+ let(:state) { 'random_string' }
+
+ subject { described_class.session_key_for_redirect_uri(state) }
+
+ it 'creates a new session key' do
+ is_expected.to eq('cloud_platform_second_redirect_uri_random_string')
+ end
+ end
+
+ describe '.new_session_key_for_redirect_uri' do
+ it 'generates a new session key' do
+ expect { |b| described_class.new_session_key_for_redirect_uri(&b) }
+ .to yield_with_args(String)
+ end
+ end
+
+ describe '#validate_token' do
+ subject { client.validate_token(expires_at) }
+
+ let(:expires_at) { 1.hour.since.utc.strftime('%s') }
+
+ context 'when token is nil' do
+ let(:token) { nil }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when expires_at is nil' do
+ let(:expires_at) { nil }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when expires in 1 hour' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when expires in 10 minutes' do
+ let(:expires_at) { 5.minutes.since.utc.strftime('%s') }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#projects_zones_clusters_get' do
+ subject { client.projects_zones_clusters_get(spy, spy, spy) }
+ let(:gke_cluster) { double }
+
+ before do
+ allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ .to receive(:get_zone_cluster).and_return(gke_cluster)
+ end
+
+ it { is_expected.to eq(gke_cluster) }
+ end
+
+ describe '#projects_zones_clusters_create' do
+ subject do
+ client.projects_zones_clusters_create(
+ spy, spy, cluster_name, cluster_size, machine_type: machine_type)
+ end
+
+ let(:cluster_name) { 'test-cluster' }
+ let(:cluster_size) { 1 }
+ let(:machine_type) { 'n1-standard-4' }
+ let(:operation) { double }
+
+ before do
+ allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ .to receive(:create_cluster).and_return(operation)
+ end
+
+ it { is_expected.to eq(operation) }
+
+ it 'sets corresponded parameters' do
+ expect_any_instance_of(Google::Apis::ContainerV1::CreateClusterRequest)
+ .to receive(:initialize).with(
+ {
+ "cluster": {
+ "name": cluster_name,
+ "initial_node_count": cluster_size,
+ "node_config": {
+ "machine_type": machine_type
+ }
+ }
+ } )
+
+ subject
+ end
+ end
+
+ describe '#projects_zones_operations' do
+ subject { client.projects_zones_operations(spy, spy, spy) }
+ let(:operation) { double }
+
+ before do
+ allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
+ .to receive(:get_zone_operation).and_return(operation)
+ end
+
+ it { is_expected.to eq(operation) }
+ end
+
+ describe '#parse_operation_id' do
+ subject { client.parse_operation_id(self_link) }
+
+ context 'when expected url' do
+ let(:self_link) do
+ 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123'
+ end
+
+ it { is_expected.to eq('ope-123') }
+ end
+
+ context 'when unexpected url' do
+ let(:self_link) { '???' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/lib/rspec_flaky/listener_spec.rb b/spec/lib/rspec_flaky/listener_spec.rb
index 7590ea9576d..bfb7648b486 100644
--- a/spec/lib/rspec_flaky/listener_spec.rb
+++ b/spec/lib/rspec_flaky/listener_spec.rb
@@ -45,6 +45,7 @@ describe RspecFlaky::Listener, :aggregate_failures do
# Stub these env variables otherwise specs don't behave the same on the CI
stub_env('CI_PROJECT_URL', nil)
stub_env('CI_JOB_ID', nil)
+ stub_env('SUITE_FLAKY_RSPEC_REPORT_PATH', nil)
end
describe '#initialize' do
diff --git a/spec/models/gcp/cluster_spec.rb b/spec/models/gcp/cluster_spec.rb
new file mode 100644
index 00000000000..350fbc257d9
--- /dev/null
+++ b/spec/models/gcp/cluster_spec.rb
@@ -0,0 +1,240 @@
+require 'spec_helper'
+
+describe Gcp::Cluster do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:service) }
+
+ it { is_expected.to validate_presence_of(:gcp_cluster_zone) }
+
+ describe '#default_value_for' do
+ let(:cluster) { described_class.new }
+
+ it { expect(cluster.gcp_cluster_zone).to eq('us-central1-a') }
+ it { expect(cluster.gcp_cluster_size).to eq(3) }
+ it { expect(cluster.gcp_machine_type).to eq('n1-standard-4') }
+ end
+
+ describe '#validates' do
+ subject { cluster.valid? }
+
+ context 'when validates gcp_project_id' do
+ let(:cluster) { build(:gcp_cluster, gcp_project_id: gcp_project_id) }
+
+ context 'when valid' do
+ let(:gcp_project_id) { 'gcp-project-12345' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when empty' do
+ let(:gcp_project_id) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when too long' do
+ let(:gcp_project_id) { 'A' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when includes abnormal character' do
+ let(:gcp_project_id) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates gcp_cluster_name' do
+ let(:cluster) { build(:gcp_cluster, gcp_cluster_name: gcp_cluster_name) }
+
+ context 'when valid' do
+ let(:gcp_cluster_name) { 'test-cluster' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when empty' do
+ let(:gcp_cluster_name) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when too long' do
+ let(:gcp_cluster_name) { 'A' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when includes abnormal character' do
+ let(:gcp_cluster_name) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates gcp_cluster_size' do
+ let(:cluster) { build(:gcp_cluster, gcp_cluster_size: gcp_cluster_size) }
+
+ context 'when valid' do
+ let(:gcp_cluster_size) { 1 }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when zero' do
+ let(:gcp_cluster_size) { 0 }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates project_namespace' do
+ let(:cluster) { build(:gcp_cluster, project_namespace: project_namespace) }
+
+ context 'when valid' do
+ let(:project_namespace) { 'default-namespace' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when empty' do
+ let(:project_namespace) { '' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when too long' do
+ let(:project_namespace) { 'A' * 64 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when includes abnormal character' do
+ let(:project_namespace) { '!!!!!!' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when validates restrict_modification' do
+ let(:cluster) { create(:gcp_cluster) }
+
+ before do
+ cluster.make_creating!
+ end
+
+ context 'when created' do
+ before do
+ cluster.make_created!
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when creating' do
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#state_machine' do
+ let(:cluster) { build(:gcp_cluster) }
+
+ context 'when transits to created state' do
+ before do
+ cluster.gcp_token = 'tmp'
+ cluster.gcp_operation_id = 'tmp'
+ cluster.make_created!
+ end
+
+ it 'nullify gcp_token and gcp_operation_id' do
+ expect(cluster.gcp_token).to be_nil
+ expect(cluster.gcp_operation_id).to be_nil
+ expect(cluster).to be_created
+ end
+ end
+
+ context 'when transits to errored state' do
+ let(:reason) { 'something wrong' }
+
+ before do
+ cluster.make_errored!(reason)
+ end
+
+ it 'sets status_reason' do
+ expect(cluster.status_reason).to eq(reason)
+ expect(cluster).to be_errored
+ end
+ end
+ end
+
+ describe '#project_namespace_placeholder' do
+ subject { cluster.project_namespace_placeholder }
+
+ let(:cluster) { create(:gcp_cluster) }
+
+ it 'returns a placeholder' do
+ is_expected.to eq("#{cluster.project.path}-#{cluster.project.id}")
+ end
+ end
+
+ describe '#on_creation?' do
+ subject { cluster.on_creation? }
+
+ let(:cluster) { create(:gcp_cluster) }
+
+ context 'when status is creating' do
+ before do
+ cluster.make_creating!
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when status is created' do
+ before do
+ cluster.make_created!
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#api_url' do
+ subject { cluster.api_url }
+
+ let(:cluster) { create(:gcp_cluster, :created_on_gke) }
+ let(:api_url) { 'https://' + cluster.endpoint }
+
+ it { is_expected.to eq(api_url) }
+ end
+
+ describe '#restrict_modification' do
+ subject { cluster.restrict_modification }
+
+ let(:cluster) { create(:gcp_cluster) }
+
+ context 'when status is created' do
+ before do
+ cluster.make_created!
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when status is creating' do
+ before do
+ cluster.make_creating!
+ end
+
+ it { is_expected.to be_falsey }
+
+ it 'sets error' do
+ is_expected.to be_falsey
+ expect(cluster.errors).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 3d6a79e0649..9b470e79a76 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -76,6 +76,7 @@ describe Project do
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
+ it { is_expected.to have_one(:cluster) }
context 'after initialized' do
it "has a project_feature" do
diff --git a/spec/policies/gcp/cluster_policy_spec.rb b/spec/policies/gcp/cluster_policy_spec.rb
new file mode 100644
index 00000000000..e213aa3d557
--- /dev/null
+++ b/spec/policies/gcp/cluster_policy_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gcp::ClusterPolicy, :models do
+ set(:project) { create(:project) }
+ set(:cluster) { create(:gcp_cluster, project: project) }
+ let(:user) { create(:user) }
+ let(:policy) { described_class.new(user, cluster) }
+
+ describe 'rules' do
+ context 'when developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when master' do
+ before do
+ project.add_master(user)
+ end
+
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+ end
+end
diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb
new file mode 100644
index 00000000000..2cf669e8191
--- /dev/null
+++ b/spec/policies/issuable_policy_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe IssuablePolicy, models: true do
+ describe '#rules' do
+ context 'when discussion is locked for the issuable' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project, discussion_locked: true) }
+ let(:policies) { described_class.new(user, issue) }
+
+ context 'when the user is not a project member' do
+ it 'can not create a note' do
+ expect(policies).to be_disallowed(:create_note)
+ end
+ end
+
+ context 'when the user is a project member' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'can create a note' do
+ expect(policies).to be_allowed(:create_note)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
new file mode 100644
index 00000000000..58d36a2c84e
--- /dev/null
+++ b/spec/policies/note_policy_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe NotePolicy, mdoels: true do
+ describe '#rules' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ def policies(noteable = nil)
+ return @policies if @policies
+
+ noteable ||= issue
+ note = create(:note, noteable: noteable, author: user, project: project)
+
+ @policies = described_class.new(user, note)
+ end
+
+ context 'when the project is public' do
+ context 'when the note author is not a project member' do
+ it 'can edit a note' do
+ expect(policies).to be_allowed(:update_note)
+ expect(policies).to be_allowed(:admin_note)
+ expect(policies).to be_allowed(:resolve_note)
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+
+ context 'when the noteable is a snippet' do
+ it 'can edit note' do
+ policies = policies(create(:project_snippet, project: project))
+
+ expect(policies).to be_allowed(:update_note)
+ expect(policies).to be_allowed(:admin_note)
+ expect(policies).to be_allowed(:resolve_note)
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+
+ context 'when a discussion is locked' do
+ before do
+ issue.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when the note author is a project member' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'can edit a note' do
+ expect(policies).to be_allowed(:update_note)
+ expect(policies).to be_allowed(:admin_note)
+ expect(policies).to be_allowed(:resolve_note)
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+
+ context 'when the note author is not a project member' do
+ it 'can not edit a note' do
+ expect(policies).to be_disallowed(:update_note)
+ expect(policies).to be_disallowed(:admin_note)
+ expect(policies).to be_disallowed(:resolve_note)
+ end
+
+ it 'can read a note' do
+ expect(policies).to be_allowed(:read_note)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/presenters/gcp/cluster_presenter_spec.rb b/spec/presenters/gcp/cluster_presenter_spec.rb
new file mode 100644
index 00000000000..8d86dc31582
--- /dev/null
+++ b/spec/presenters/gcp/cluster_presenter_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gcp::ClusterPresenter do
+ let(:project) { create(:project) }
+ let(:cluster) { create(:gcp_cluster, project: project) }
+
+ subject(:presenter) do
+ described_class.new(cluster)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Delegated' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
+ end
+
+ describe '#initialize' do
+ it 'takes a cluster and optional params' do
+ expect { presenter }.not_to raise_error
+ end
+
+ it 'exposes cluster' do
+ expect(presenter.cluster).to eq(cluster)
+ end
+
+ it 'forwards missing methods to cluster' do
+ expect(presenter.gcp_cluster_zone).to eq(cluster.gcp_cluster_zone)
+ end
+ end
+
+ describe '#gke_cluster_url' do
+ subject { described_class.new(cluster).gke_cluster_url }
+
+ it { is_expected.to include(cluster.gcp_cluster_zone) }
+ it { is_expected.to include(cluster.gcp_cluster_name) }
+ end
+end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index f5882c0c74a..fb440fa551c 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -302,6 +302,40 @@ describe API::Notes do
expect(private_issue.notes.reload).to be_empty
end
end
+
+ context 'when the merge request discussion is locked' do
+ before do
+ merge_request.update_attribute(:discussion_locked, true)
+ end
+
+ context 'when a user is a team member' do
+ subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user), body: 'Hi!' }
+
+ it 'returns 200 status' do
+ subject
+
+ expect(response).to have_http_status(201)
+ end
+
+ it 'creates a new note' do
+ expect { subject }.to change { Note.count }.by(1)
+ end
+ end
+
+ context 'when a user is not a team member' do
+ subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", private_user), body: 'Hi!' }
+
+ it 'returns 403 status' do
+ subject
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not create a new note' do
+ expect { subject }.not_to change { Note.count }
+ end
+ end
+ end
end
describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
new file mode 100644
index 00000000000..2c7f49974f1
--- /dev/null
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe ClusterEntity do
+ set(:cluster) { create(:gcp_cluster, :errored) }
+ let(:request) { double('request') }
+
+ let(:entity) do
+ described_class.new(cluster)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains status' do
+ expect(subject[:status]).to eq(:errored)
+ end
+
+ it 'contains status reason' do
+ expect(subject[:status_reason]).to eq('general error')
+ end
+ end
+end
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
new file mode 100644
index 00000000000..1ac6784d28f
--- /dev/null
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe ClusterSerializer do
+ let(:serializer) do
+ described_class.new
+ end
+
+ describe '#represent_status' do
+ subject { serializer.represent_status(resource) }
+
+ context 'when represents only status' do
+ let(:resource) { create(:gcp_cluster, :errored) }
+
+ it 'serializes only status' do
+ expect(subject.keys).to contain_exactly(:status, :status_reason)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_cluster_service_spec.rb b/spec/services/ci/create_cluster_service_spec.rb
new file mode 100644
index 00000000000..6e7398fbffa
--- /dev/null
+++ b/spec/services/ci/create_cluster_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Ci::CreateClusterService do
+ describe '#execute' do
+ let(:access_token) { 'xxx' }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:result) { described_class.new(project, user, params).execute(access_token) }
+
+ context 'when correct params' do
+ let(:params) do
+ {
+ gcp_project_id: 'gcp-project',
+ gcp_cluster_name: 'test-cluster',
+ gcp_cluster_zone: 'us-central1-a',
+ gcp_cluster_size: 1
+ }
+ end
+
+ it 'creates a cluster object' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { result }.to change { Gcp::Cluster.count }.by(1)
+ expect(result.gcp_project_id).to eq('gcp-project')
+ expect(result.gcp_cluster_name).to eq('test-cluster')
+ expect(result.gcp_cluster_zone).to eq('us-central1-a')
+ expect(result.gcp_cluster_size).to eq(1)
+ expect(result.gcp_token).to eq(access_token)
+ end
+ end
+
+ context 'when invalid params' do
+ let(:params) do
+ {
+ gcp_project_id: 'gcp-project',
+ gcp_cluster_name: 'test-cluster',
+ gcp_cluster_zone: 'us-central1-a',
+ gcp_cluster_size: 'ABC'
+ }
+ end
+
+ it 'returns an error' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { result }.to change { Gcp::Cluster.count }.by(0)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/fetch_gcp_operation_service_spec.rb b/spec/services/ci/fetch_gcp_operation_service_spec.rb
new file mode 100644
index 00000000000..7792979c5cb
--- /dev/null
+++ b/spec/services/ci/fetch_gcp_operation_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+require 'google/apis'
+
+describe Ci::FetchGcpOperationService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:operation) { double }
+
+ context 'when suceeded' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_operations).and_return(operation)
+ end
+
+ it 'fetch the gcp operaion' do
+ expect { |b| described_class.new.execute(cluster, &b) }
+ .to yield_with_args(operation)
+ end
+ end
+
+ context 'when raises an error' do
+ let(:error) { Google::Apis::ServerError.new('a') }
+
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_operations).and_raise(error)
+ end
+
+ it 'sets an error to cluster object' do
+ expect { |b| described_class.new.execute(cluster, &b) }
+ .not_to yield_with_args
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/fetch_kubernetes_token_service_spec.rb b/spec/services/ci/fetch_kubernetes_token_service_spec.rb
new file mode 100644
index 00000000000..1d05c9671a9
--- /dev/null
+++ b/spec/services/ci/fetch_kubernetes_token_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Ci::FetchKubernetesTokenService do
+ describe '#execute' do
+ subject { described_class.new(api_url, ca_pem, username, password).execute }
+
+ let(:api_url) { 'http://111.111.111.111' }
+ let(:ca_pem) { '' }
+ let(:username) { 'admin' }
+ let(:password) { 'xxx' }
+
+ context 'when params correct' do
+ let(:token) { 'xxx.token.xxx' }
+
+ let(:secrets_json) do
+ [
+ {
+ 'metadata': {
+ name: metadata_name
+ },
+ 'data': {
+ 'token': Base64.encode64(token)
+ }
+ }
+ ]
+ end
+
+ before do
+ allow_any_instance_of(Kubeclient::Client)
+ .to receive(:get_secrets).and_return(secrets_json)
+ end
+
+ context 'when default-token exists' do
+ let(:metadata_name) { 'default-token-123' }
+
+ it { is_expected.to eq(token) }
+ end
+
+ context 'when default-token does not exist' do
+ let(:metadata_name) { 'another-token-123' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'when api_url is nil' do
+ let(:api_url) { nil }
+
+ it { expect { subject }.to raise_error("Incomplete settings") }
+ end
+
+ context 'when username is nil' do
+ let(:username) { nil }
+
+ it { expect { subject }.to raise_error("Incomplete settings") }
+ end
+
+ context 'when password is nil' do
+ let(:password) { nil }
+
+ it { expect { subject }.to raise_error("Incomplete settings") }
+ end
+ end
+end
diff --git a/spec/services/ci/finalize_cluster_creation_service_spec.rb b/spec/services/ci/finalize_cluster_creation_service_spec.rb
new file mode 100644
index 00000000000..def3709fdb4
--- /dev/null
+++ b/spec/services/ci/finalize_cluster_creation_service_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Ci::FinalizeClusterCreationService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:result) { described_class.new.execute(cluster) }
+
+ context 'when suceeded to get cluster from api' do
+ let(:gke_cluster) { double }
+
+ before do
+ allow(gke_cluster).to receive(:endpoint).and_return('111.111.111.111')
+ allow(gke_cluster).to receive(:master_auth).and_return(spy)
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_get).and_return(gke_cluster)
+ end
+
+ context 'when suceeded to get kubernetes token' do
+ let(:kubernetes_token) { 'abc' }
+
+ before do
+ allow_any_instance_of(Ci::FetchKubernetesTokenService)
+ .to receive(:execute).and_return(kubernetes_token)
+ end
+
+ it 'executes integration cluster' do
+ expect_any_instance_of(Ci::IntegrateClusterService).to receive(:execute)
+ described_class.new.execute(cluster)
+ end
+ end
+
+ context 'when failed to get kubernetes token' do
+ before do
+ allow_any_instance_of(Ci::FetchKubernetesTokenService)
+ .to receive(:execute).and_return(nil)
+ end
+
+ it 'sets an error to cluster object' do
+ described_class.new.execute(cluster)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+
+ context 'when failed to get cluster from api' do
+ let(:error) { Google::Apis::ServerError.new('a') }
+
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_get).and_raise(error)
+ end
+
+ it 'sets an error to cluster object' do
+ described_class.new.execute(cluster)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/integrate_cluster_service_spec.rb b/spec/services/ci/integrate_cluster_service_spec.rb
new file mode 100644
index 00000000000..3a79c205bd1
--- /dev/null
+++ b/spec/services/ci/integrate_cluster_service_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Ci::IntegrateClusterService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster, :custom_project_namespace) }
+ let(:endpoint) { '123.123.123.123' }
+ let(:ca_cert) { 'ca_cert_xxx' }
+ let(:token) { 'token_xxx' }
+ let(:username) { 'username_xxx' }
+ let(:password) { 'password_xxx' }
+
+ before do
+ described_class
+ .new.execute(cluster, endpoint, ca_cert, token, username, password)
+
+ cluster.reload
+ end
+
+ context 'when correct params' do
+ it 'creates a cluster object' do
+ expect(cluster.endpoint).to eq(endpoint)
+ expect(cluster.ca_cert).to eq(ca_cert)
+ expect(cluster.kubernetes_token).to eq(token)
+ expect(cluster.username).to eq(username)
+ expect(cluster.password).to eq(password)
+ expect(cluster.service.active).to be_truthy
+ expect(cluster.service.api_url).to eq(cluster.api_url)
+ expect(cluster.service.ca_pem).to eq(ca_cert)
+ expect(cluster.service.namespace).to eq(cluster.project_namespace)
+ expect(cluster.service.token).to eq(token)
+ end
+ end
+
+ context 'when invalid params' do
+ let(:endpoint) { nil }
+
+ it 'sets an error to cluster object' do
+ expect(cluster).to be_errored
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/provision_cluster_service_spec.rb b/spec/services/ci/provision_cluster_service_spec.rb
new file mode 100644
index 00000000000..5ce5c788314
--- /dev/null
+++ b/spec/services/ci/provision_cluster_service_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Ci::ProvisionClusterService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:operation) { spy }
+
+ shared_examples 'error' do
+ it 'sets an error to cluster object' do
+ described_class.new.execute(cluster)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+
+ context 'when suceeded to request provision' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create).and_return(operation)
+ end
+
+ context 'when operation status is RUNNING' do
+ before do
+ allow(operation).to receive(:status).and_return('RUNNING')
+ end
+
+ context 'when suceeded to parse gcp operation id' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:parse_operation_id).and_return('operation-123')
+ end
+
+ context 'when cluster status is scheduled' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:parse_operation_id).and_return('operation-123')
+ end
+
+ it 'schedules a worker for status minitoring' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+
+ described_class.new.execute(cluster)
+ end
+ end
+
+ context 'when cluster status is creating' do
+ before do
+ cluster.make_creating!
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when failed to parse gcp operation id' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:parse_operation_id).and_return(nil)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when operation status is others' do
+ before do
+ allow(operation).to receive(:status).and_return('others')
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+
+ context 'when failed to request provision' do
+ let(:error) { Google::Apis::ServerError.new('a') }
+
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create).and_raise(error)
+ end
+
+ it_behaves_like 'error'
+ end
+ end
+end
diff --git a/spec/services/ci/update_cluster_service_spec.rb b/spec/services/ci/update_cluster_service_spec.rb
new file mode 100644
index 00000000000..a289385b88f
--- /dev/null
+++ b/spec/services/ci/update_cluster_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Ci::UpdateClusterService do
+ describe '#execute' do
+ let(:cluster) { create(:gcp_cluster, :created_on_gke, :with_kubernetes_service) }
+
+ before do
+ described_class.new(cluster.project, cluster.user, params).execute(cluster)
+
+ cluster.reload
+ end
+
+ context 'when correct params' do
+ context 'when enabled is true' do
+ let(:params) { { 'enabled' => 'true' } }
+
+ it 'enables cluster and overwrite kubernetes service' do
+ expect(cluster.enabled).to be_truthy
+ expect(cluster.service.active).to be_truthy
+ expect(cluster.service.api_url).to eq(cluster.api_url)
+ expect(cluster.service.ca_pem).to eq(cluster.ca_cert)
+ expect(cluster.service.namespace).to eq(cluster.project_namespace)
+ expect(cluster.service.token).to eq(cluster.kubernetes_token)
+ end
+ end
+
+ context 'when enabled is false' do
+ let(:params) { { 'enabled' => 'false' } }
+
+ it 'disables cluster and kubernetes service' do
+ expect(cluster.enabled).to be_falsy
+ expect(cluster.service.active).to be_falsy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index a8a8aeed1bd..bdbe0a353fb 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -48,7 +48,8 @@ describe Issues::UpdateService, :mailer do
assignee_ids: [user2.id],
state_event: 'close',
label_ids: [label.id],
- due_date: Date.tomorrow
+ due_date: Date.tomorrow,
+ discussion_locked: true
}
end
@@ -62,6 +63,7 @@ describe Issues::UpdateService, :mailer do
expect(issue).to be_closed
expect(issue.labels).to match_array [label]
expect(issue.due_date).to eq Date.tomorrow
+ expect(issue.discussion_locked).to be_truthy
end
it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do
@@ -110,6 +112,7 @@ describe Issues::UpdateService, :mailer do
expect(issue.labels).to be_empty
expect(issue.milestone).to be_nil
expect(issue.due_date).to be_nil
+ expect(issue.discussion_locked).to be_falsey
end
end
@@ -148,6 +151,13 @@ describe Issues::UpdateService, :mailer do
expect(note).not_to be_nil
expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**'
end
+
+ it 'creates system note about discussion lock' do
+ note = find_note('locked this issue')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq 'locked this issue'
+ end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 681feee61d1..b11a1b31f32 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -49,7 +49,8 @@ describe MergeRequests::UpdateService, :mailer do
state_event: 'close',
label_ids: [label.id],
target_branch: 'target',
- force_remove_source_branch: '1'
+ force_remove_source_branch: '1',
+ discussion_locked: true
}
end
@@ -73,6 +74,7 @@ describe MergeRequests::UpdateService, :mailer do
expect(@merge_request.labels.first.title).to eq(label.name)
expect(@merge_request.target_branch).to eq('target')
expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1')
+ expect(@merge_request.discussion_locked).to be_truthy
end
it 'executes hooks with update action' do
@@ -123,6 +125,13 @@ describe MergeRequests::UpdateService, :mailer do
expect(note.note).to eq 'changed target branch from `master` to `target`'
end
+ it 'creates system note about discussion lock' do
+ note = find_note('locked this issue')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq 'locked this issue'
+ end
+
context 'when not including source branch removal options' do
before do
opts.delete(:force_remove_source_branch)
diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb
new file mode 100644
index 00000000000..fd22e384b1b
--- /dev/null
+++ b/spec/support/helpers/merge_request_diff_helpers.rb
@@ -0,0 +1,28 @@
+module MergeRequestDiffHelpers
+ def click_diff_line(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+ line[:num].find('.add-diff-note').trigger('click')
+ end
+
+ def get_line_components(line_holder, diff_side = nil)
+ if diff_side.nil?
+ get_inline_line_components(line_holder)
+ else
+ get_parallel_line_components(line_holder, diff_side)
+ end
+ end
+
+ def get_inline_line_components(line_holder)
+ { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
+ end
+
+ def get_parallel_line_components(line_holder, diff_side = nil)
+ side_index = diff_side == 'left' ? 0 : 1
+ # Wait for `.line_content`
+ line_holder.find('.line_content', match: :first)
+ # Wait for `.diff-line-num`
+ line_holder.find('.diff-line-num', match: :first)
+ { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
+ end
+end
diff --git a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
new file mode 100644
index 00000000000..221926aaf7e
--- /dev/null
+++ b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
@@ -0,0 +1,28 @@
+shared_examples 'comment on merge request file' do
+ it 'adds a comment' do
+ click_diff_line(find("[id='#{sample_commit.line_code}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in(:note_note, with: 'Line is wrong')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ page.within('.notes_holder') do
+ expect(page).to have_content('Line is wrong')
+ end
+
+ visit(merge_request_path(merge_request))
+
+ page.within('.notes .discussion') do
+ expect(page).to have_content("#{user.name} #{user.to_reference} started a discussion")
+ expect(page).to have_content(sample_commit.line_code_path)
+ expect(page).to have_content('Line is wrong')
+ end
+
+ page.within('.notes-tab .badge') do
+ expect(page).to have_content('1')
+ end
+ end
+end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
new file mode 100644
index 00000000000..11f208289db
--- /dev/null
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe ClusterProvisionWorker do
+ describe '#perform' do
+ context 'when cluster exists' do
+ let(:cluster) { create(:gcp_cluster) }
+
+ it 'provision a cluster' do
+ expect_any_instance_of(Ci::ProvisionClusterService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when cluster does not exist' do
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Ci::ProvisionClusterService).not_to receive(:execute)
+
+ described_class.new.perform(123)
+ end
+ end
+ end
+end
diff --git a/spec/workers/concerns/cluster_queue_spec.rb b/spec/workers/concerns/cluster_queue_spec.rb
new file mode 100644
index 00000000000..1050651fa51
--- /dev/null
+++ b/spec/workers/concerns/cluster_queue_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe ClusterQueue do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ include ClusterQueue
+ end
+ end
+
+ it 'sets a default pipelines queue automatically' do
+ expect(worker.sidekiq_options['queue'])
+ .to eq :gcp_cluster
+ end
+end
diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb
new file mode 100644
index 00000000000..dcd4a3b9aec
--- /dev/null
+++ b/spec/workers/wait_for_cluster_creation_worker_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe WaitForClusterCreationWorker do
+ describe '#perform' do
+ context 'when cluster exists' do
+ let(:cluster) { create(:gcp_cluster) }
+ let(:operation) { double }
+
+ before do
+ allow(operation).to receive(:status).and_return(status)
+ allow(operation).to receive(:start_time).and_return(1.minute.ago)
+ allow(operation).to receive(:status_message).and_return('error')
+ allow_any_instance_of(Ci::FetchGcpOperationService).to receive(:execute).and_yield(operation)
+ end
+
+ context 'when operation status is RUNNING' do
+ let(:status) { 'RUNNING' }
+
+ it 'reschedules worker' do
+ expect(described_class).to receive(:perform_in)
+
+ described_class.new.perform(cluster.id)
+ end
+
+ context 'when operation timeout' do
+ before do
+ allow(operation).to receive(:start_time).and_return(30.minutes.ago.utc)
+ end
+
+ it 'sets an error message on cluster' do
+ described_class.new.perform(cluster.id)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+
+ context 'when operation status is DONE' do
+ let(:status) { 'DONE' }
+
+ it 'finalizes cluster creation' do
+ expect_any_instance_of(Ci::FinalizeClusterCreationService).to receive(:execute)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when operation status is others' do
+ let(:status) { 'others' }
+
+ it 'sets an error message on cluster' do
+ described_class.new.perform(cluster.id)
+
+ expect(cluster.reload).to be_errored
+ end
+ end
+ end
+
+ context 'when cluster does not exist' do
+ it 'does not provision a cluster' do
+ expect_any_instance_of(Ci::FetchGcpOperationService).not_to receive(:execute)
+
+ described_class.new.perform(1234)
+ end
+ end
+ end
+end