summaryrefslogtreecommitdiff
path: root/spec/support
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/api/repositories_shared_context.rb10
-rw-r--r--spec/support/api/status_shared_examples.rb42
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb132
-rw-r--r--spec/support/capybara.rb10
-rw-r--r--spec/support/carrierwave.rb2
-rw-r--r--spec/support/chat_slash_commands_shared_examples.rb97
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_context.rb10
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb232
-rw-r--r--spec/support/cycle_analytics_helpers.rb3
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb13
-rw-r--r--spec/support/database_connection_helpers.rb9
-rw-r--r--spec/support/db_cleaner.rb4
-rw-r--r--spec/support/email_helpers.rb28
-rw-r--r--spec/support/fake_u2f_device.rb3
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb32
-rw-r--r--spec/support/import_export/common_util.rb10
-rw-r--r--spec/support/javascript_fixtures_helpers.rb28
-rw-r--r--spec/support/jira_service_helper.rb8
-rw-r--r--spec/support/kubernetes_helpers.rb52
-rw-r--r--spec/support/login_helpers.rb3
-rw-r--r--spec/support/matchers/access_matchers.rb36
-rw-r--r--spec/support/matchers/be_url.rb5
-rw-r--r--spec/support/matchers/be_valid_commit.rb8
-rw-r--r--spec/support/matchers/have_issuable_counts.rb8
-rw-r--r--spec/support/matchers/is_within.rb9
-rw-r--r--spec/support/matchers/markdown_matchers.rb6
-rw-r--r--spec/support/notify_shared_examples.rb19
-rw-r--r--spec/support/project_features_apply_to_issuables_shared_examples.rb56
-rw-r--r--spec/support/query_recorder.rb40
-rw-r--r--spec/support/rake_helpers.rb4
-rw-r--r--spec/support/reactive_caching_helpers.rb42
-rw-r--r--spec/support/reference_parser_shared_examples.rb43
-rw-r--r--spec/support/search_helpers.rb5
-rw-r--r--spec/support/seed_helper.rb112
-rw-r--r--spec/support/seed_repo.rb143
-rw-r--r--spec/support/services/issuable_create_service_shared_examples.rb52
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb4
-rw-r--r--spec/support/services/issuable_update_service_shared_examples.rb69
-rw-r--r--spec/support/services_shared_context.rb6
-rw-r--r--spec/support/setup_builds_storage.rb17
-rw-r--r--spec/support/slack_mattermost_notifications_shared_examples.rb328
-rw-r--r--spec/support/stub_env.rb7
-rw-r--r--spec/support/test_env.rb27
-rw-r--r--spec/support/time_tracking_shared_examples.rb82
-rw-r--r--spec/support/upload_helpers.rb16
45 files changed, 1779 insertions, 93 deletions
diff --git a/spec/support/api/repositories_shared_context.rb b/spec/support/api/repositories_shared_context.rb
new file mode 100644
index 00000000000..ea38fe4f5b8
--- /dev/null
+++ b/spec/support/api/repositories_shared_context.rb
@@ -0,0 +1,10 @@
+shared_context 'disabled repository' do
+ before do
+ project.project_feature.update_attributes!(
+ repository_access_level: ProjectFeature::DISABLED,
+ merge_requests_access_level: ProjectFeature::DISABLED,
+ builds_access_level: ProjectFeature::DISABLED
+ )
+ expect(project.feature_available?(:repository, current_user)).to be false
+ end
+end
diff --git a/spec/support/api/status_shared_examples.rb b/spec/support/api/status_shared_examples.rb
new file mode 100644
index 00000000000..3481749a7f0
--- /dev/null
+++ b/spec/support/api/status_shared_examples.rb
@@ -0,0 +1,42 @@
+# Specs for status checking.
+#
+# Requires an API request:
+# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
+shared_examples_for '400 response' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'returns 400' do
+ expect(response).to have_http_status(400)
+ end
+end
+
+shared_examples_for '403 response' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'returns 403' do
+ expect(response).to have_http_status(403)
+ end
+end
+
+shared_examples_for '404 response' do
+ let(:message) { nil }
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'returns 404' do
+ expect(response).to have_http_status(404)
+ expect(json_response).to be_an Object
+
+ if message.present?
+ expect(json_response['message']).to eq(message)
+ end
+ end
+end
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
new file mode 100644
index 00000000000..210cd5817e0
--- /dev/null
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -0,0 +1,132 @@
+shared_examples 'an unauthorized API user' do
+ it { is_expected.to eq(403) }
+end
+
+shared_examples 'time tracking endpoints' do |issuable_name|
+ issuable_collection_name = issuable_name.pluralize
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
+ context 'with an unauthorized user' do
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "sets the time estimate for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['human_time_estimate']).to eq('1w')
+ end
+
+ describe 'updating the current estimate' do
+ before do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w'
+ end
+
+ context 'when duration has a bad format' do
+ it 'does not modify the original estimate' do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(issuable.reload.human_time_estimate).to eq('1w')
+ end
+ end
+
+ context 'with a valid duration' do
+ it 'updates the estimate' do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h'
+
+ expect(response).to have_http_status(200)
+ expect(issuable.reload.human_time_estimate).to eq('3w 1h')
+ end
+ end
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do
+ context 'with an unauthorized user' do
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "resets the time estimate for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['time_estimate']).to eq(0)
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do
+ context 'with an unauthorized user' do
+ subject do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member),
+ duration: '2h'
+ end
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "add spent time for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '2h'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['human_total_time_spent']).to eq('2h')
+ end
+
+ context 'when subtracting time' do
+ it 'subtracts time of the total spent time' do
+ issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '-1h'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['total_time_spent']).to eq(3600)
+ end
+ end
+
+ context 'when time to subtract is greater than the total spent time' do
+ it 'does not modify the total time spent' do
+ issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
+ duration: '-1w'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do
+ context 'with an unauthorized user' do
+ subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) }
+
+ it_behaves_like 'an unauthorized API user'
+ end
+
+ it "resets spent time for #{issuable_name}" do
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['total_time_spent']).to eq(0)
+ end
+ end
+
+ describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
+ it "returns the time stats for #{issuable_name}" do
+ issuable.update_attributes!(spend_time: { duration: 1800, user: user },
+ time_estimate: 3600)
+
+ get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['total_time_spent']).to eq(1800)
+ expect(json_response['time_estimate']).to eq(3600)
+ end
+ end
+end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index e1f90e17cce..16d5f2bf0b8 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -7,7 +7,15 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768])
+ Capybara::Poltergeist::Driver.new(
+ app,
+ js_errors: true,
+ timeout: timeout,
+ window_size: [1366, 768],
+ phantomjs_options: [
+ '--load-images=no'
+ ]
+ )
end
Capybara.default_max_wait_time = timeout
diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb
index aa89afd8fb3..72af2c70324 100644
--- a/spec/support/carrierwave.rb
+++ b/spec/support/carrierwave.rb
@@ -1,7 +1,7 @@
CarrierWave.root = 'tmp/tests/uploads'
RSpec.configure do |config|
- config.after(:suite) do
+ config.after(:each) do
FileUtils.rm_rf('tmp/tests/uploads')
end
end
diff --git a/spec/support/chat_slash_commands_shared_examples.rb b/spec/support/chat_slash_commands_shared_examples.rb
new file mode 100644
index 00000000000..4dfa29849ee
--- /dev/null
+++ b/spec/support/chat_slash_commands_shared_examples.rb
@@ -0,0 +1,97 @@
+RSpec.shared_examples 'chat slash commands service' do
+ describe "Associations" do
+ it { is_expected.to respond_to :token }
+ it { is_expected.to have_many :chat_names }
+ end
+
+ describe '#valid_token?' do
+ subject { described_class.new }
+
+ context 'when the token is empty' do
+ it 'is false' do
+ expect(subject.valid_token?('wer')).to be_falsey
+ end
+ end
+
+ context 'when there is a token' do
+ before do
+ subject.token = '123'
+ end
+
+ it 'accepts equal tokens' do
+ expect(subject.valid_token?('123')).to be_truthy
+ end
+ end
+ end
+
+ describe '#trigger' do
+ subject { described_class.new }
+
+ context 'no token is passed' do
+ let(:params) { Hash.new }
+
+ it 'returns nil' do
+ expect(subject.trigger(params)).to be_nil
+ end
+ end
+
+ context 'with a token passed' do
+ let(:project) { create(:empty_project) }
+ let(:params) { { token: 'token' } }
+
+ before do
+ allow(subject).to receive(:token).and_return('token')
+ end
+
+ context 'no user can be found' do
+ context 'when no url can be generated' do
+ it 'responds with the authorize url' do
+ response = subject.trigger(params)
+
+ expect(response[:response_type]).to eq :ephemeral
+ expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you"
+ end
+ end
+
+ context 'when an auth url can be generated' do
+ let(:params) do
+ {
+ team_domain: 'http://domain.tld',
+ team_id: 'T3423423',
+ user_id: 'U234234',
+ user_name: 'mepmep',
+ token: 'token'
+ }
+ end
+
+ let(:service) do
+ project.create_mattermost_slash_commands_service(
+ properties: { token: 'token' }
+ )
+ end
+
+ it 'generates the url' do
+ response = service.trigger(params)
+
+ expect(response[:text]).to start_with(':wave: Hi there!')
+ end
+ end
+ end
+
+ context 'when the user is authenticated' do
+ let!(:chat_name) { create(:chat_name, service: subject) }
+ let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
+
+ subject do
+ described_class.create(project: project, properties: { token: 'token' })
+ end
+
+ it 'triggers the command' do
+ expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute)
+
+ subject.trigger(params)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/controllers/githubish_import_controller_shared_context.rb b/spec/support/controllers/githubish_import_controller_shared_context.rb
new file mode 100644
index 00000000000..e71994edec6
--- /dev/null
+++ b/spec/support/controllers/githubish_import_controller_shared_context.rb
@@ -0,0 +1,10 @@
+shared_context 'a GitHub-ish import controller' do
+ let(:user) { create(:user) }
+ let(:token) { "asdasd12345" }
+ let(:access_params) { { github_access_token: token } }
+
+ before do
+ sign_in(user)
+ allow(controller).to receive(:"#{provider}_import_enabled?").and_return(true)
+ end
+end
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
new file mode 100644
index 00000000000..d0fd2d52004
--- /dev/null
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -0,0 +1,232 @@
+# Specifications for behavior common to all objects with an email attribute.
+# Takes a list of email-format attributes and requires:
+# - subject { "the object with a attribute= setter" }
+# Note: You have access to `email_value` which is the email address value
+# being currently tested).
+
+def assign_session_token(provider)
+ session[:"#{provider}_access_token"] = 'asdasd12345'
+end
+
+shared_examples 'a GitHub-ish import controller: POST personal_access_token' do
+ let(:status_import_url) { public_send("status_import_#{provider}_url") }
+
+ it "updates access token" do
+ token = 'asdfasdf9876'
+
+ allow_any_instance_of(Gitlab::GithubImport::Client).
+ to receive(:user).and_return(true)
+
+ post :personal_access_token, personal_access_token: token
+
+ expect(session[:"#{provider}_access_token"]).to eq(token)
+ expect(controller).to redirect_to(status_import_url)
+ end
+end
+
+shared_examples 'a GitHub-ish import controller: GET new' do
+ let(:status_import_url) { public_send("status_import_#{provider}_url") }
+
+ it "redirects to status if we already have a token" do
+ assign_session_token(provider)
+ allow(controller).to receive(:logged_in_with_provider?).and_return(false)
+
+ get :new
+
+ expect(controller).to redirect_to(status_import_url)
+ end
+
+ it "renders the :new page if no token is present in session" do
+ get :new
+
+ expect(response).to render_template(:new)
+ end
+end
+
+shared_examples 'a GitHub-ish import controller: GET status' do
+ let(:new_import_url) { public_send("new_import_#{provider}_url") }
+ let(:user) { create(:user) }
+ let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') }
+ let(:org) { OpenStruct.new(login: 'company') }
+ let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') }
+ let(:extra_assign_expectations) { {} }
+
+ before do
+ assign_session_token(provider)
+ end
+
+ it "assigns variables" do
+ project = create(:empty_project, import_type: provider, creator_id: user.id)
+ stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([project])
+ expect(assigns(:repos)).to eq([repo, org_repo])
+ extra_assign_expectations.each do |key, value|
+ expect(assigns(key)).to eq(value)
+ end
+ end
+
+ it "does not show already added project" do
+ project = create(:empty_project, import_type: provider, creator_id: user.id, import_source: 'asd/vim')
+ stub_client(repos: [repo], orgs: [])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([project])
+ expect(assigns(:repos)).to eq([])
+ end
+
+ it "handles an invalid access token" do
+ allow_any_instance_of(Gitlab::GithubImport::Client).
+ to receive(:repos).and_raise(Octokit::Unauthorized)
+
+ get :status
+
+ expect(session[:"#{provider}_access_token"]).to be_nil
+ expect(controller).to redirect_to(new_import_url)
+ expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.")
+ end
+end
+
+shared_examples 'a GitHub-ish import controller: POST create' do
+ let(:user) { create(:user) }
+ let(:provider_username) { user.username }
+ let(:provider_user) { OpenStruct.new(login: provider_username) }
+ let(:provider_repo) do
+ OpenStruct.new(
+ name: 'vim',
+ full_name: "#{provider_username}/vim",
+ owner: OpenStruct.new(login: provider_username)
+ )
+ end
+
+ before do
+ stub_client(user: provider_user, repo: provider_repo)
+ assign_session_token(provider)
+ end
+
+ context "when the repository owner is the provider user" do
+ context "when the provider user and GitLab user's usernames match" do
+ it "takes the current user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+
+ context "when the provider user and GitLab user's usernames don't match" do
+ let(:provider_username) { "someone_else" }
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+ end
+
+ context "when the repository owner is not the provider user" do
+ let(:other_username) { "someone_else" }
+
+ before do
+ provider_repo.owner = OpenStruct.new(login: other_username)
+ assign_session_token(provider)
+ end
+
+ context "when a namespace with the provider user's username already exists" do
+ let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
+
+ context "when the namespace is owned by the GitLab user" do
+ it "takes the existing namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+
+ context "when the namespace is not owned by the GitLab user" do
+ before do
+ existing_namespace.owner = create(:user)
+ existing_namespace.save
+ end
+
+ it "creates a project using user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+ end
+
+ context "when a namespace with the provider user's username doesn't exist" do
+ context "when current user can create namespaces" do
+ it "creates the namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ expect { post :create, target_namespace: provider_repo.name, format: :js }.to change(Namespace, :count).by(1)
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, target_namespace: provider_repo.name, format: :js
+ end
+ end
+
+ context "when current user can't create namespaces" do
+ before do
+ user.update_attribute(:can_create_group, false)
+ end
+
+ it "doesn't create the namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ expect { post :create, format: :js }.not_to change(Namespace, :count)
+ end
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+ end
+
+ context 'user has chosen a namespace and name for the project' do
+ let(:test_namespace) { create(:namespace, name: 'test_namespace', owner: user) }
+ let(:test_name) { 'test_name' }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: test_namespace.name, new_name: test_name, format: :js }
+ end
+
+ it 'takes the selected name and default namespace' do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, { new_name: test_name, format: :js }
+ end
+ end
+ end
+end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 62a5b46d47b..75c95d70951 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -49,7 +49,8 @@ module CycleAnalyticsHelpers
end
def merge_merge_requests_closing_issue(issue)
- merge_requests = issue.closed_by_merge_requests
+ merge_requests = issue.closed_by_merge_requests(user)
+
merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
end
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
index 8e19a6c92e2..35b40d73191 100644
--- a/spec/support/cycle_analytics_helpers/test_generation.rb
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -2,7 +2,6 @@
# Note: The ABC size is large here because we have a method generating test cases with
# multiple nested contexts. This shouldn't count as a violation.
-
module CycleAnalyticsHelpers
module TestGeneration
# Generate the most common set of specs that all cycle analytics phases need to have.
@@ -51,7 +50,7 @@ module CycleAnalyticsHelpers
end
median_time_difference = time_differences.sort[2]
- expect(subject.send(phase)).to be_within(5).of(median_time_difference)
+ expect(subject[phase].median).to be_within(5).of(median_time_difference)
end
context "when the data belongs to another project" do
@@ -83,7 +82,7 @@ module CycleAnalyticsHelpers
# Turn off the stub before checking assertions
allow(self).to receive(:project).and_call_original
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
@@ -106,7 +105,7 @@ module CycleAnalyticsHelpers
Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
end
@@ -126,7 +125,7 @@ module CycleAnalyticsHelpers
Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
end
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
end
@@ -145,7 +144,7 @@ module CycleAnalyticsHelpers
post_fn[self, data] if post_fn
end
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
end
@@ -153,7 +152,7 @@ module CycleAnalyticsHelpers
context "when none of the start / end conditions are matched" do
it "returns nil" do
- expect(subject.send(phase)).to be_nil
+ expect(subject[phase].median).to be_nil
end
end
end
diff --git a/spec/support/database_connection_helpers.rb b/spec/support/database_connection_helpers.rb
new file mode 100644
index 00000000000..763329499f0
--- /dev/null
+++ b/spec/support/database_connection_helpers.rb
@@ -0,0 +1,9 @@
+module DatabaseConnectionHelpers
+ def run_with_new_database_connection
+ pool = ActiveRecord::Base.connection_pool
+ conn = pool.checkout
+ yield conn
+ ensure
+ pool.checkin(conn)
+ end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index ac38e31b77e..247f0954221 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -11,6 +11,10 @@ RSpec.configure do |config|
DatabaseCleaner.strategy = :truncation
end
+ config.before(:each, truncate: true) do
+ DatabaseCleaner.strategy = :truncation
+ end
+
config.before(:each) do
DatabaseCleaner.start
end
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index 0bfc4685532..3e979f2f470 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -1,23 +1,33 @@
module EmailHelpers
- def sent_to_user?(user)
- ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
+ def sent_to_user?(user, recipients = email_recipients)
+ recipients.include?(user.notification_email)
end
def reset_delivered_emails!
ActionMailer::Base.deliveries.clear
end
- def should_only_email(*users)
- users.each {|user| should_email(user) }
- recipients = ActionMailer::Base.deliveries.flat_map(&:to)
+ def should_only_email(*users, kind: :to)
+ recipients = email_recipients(kind: kind)
+
+ users.each { |user| should_email(user, recipients) }
+
expect(recipients.count).to eq(users.count)
end
- def should_email(user)
- expect(sent_to_user?(user)).to be_truthy
+ def should_email(user, recipients = email_recipients)
+ expect(sent_to_user?(user, recipients)).to be_truthy
+ end
+
+ def should_not_email(user, recipients = email_recipients)
+ expect(sent_to_user?(user, recipients)).to be_falsey
+ end
+
+ def should_not_email_anyone
+ expect(ActionMailer::Base.deliveries).to be_empty
end
- def should_not_email(user)
- expect(sent_to_user?(user)).to be_falsey
+ def email_recipients(kind: :to)
+ ActionMailer::Base.deliveries.flat_map(&kind)
end
end
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb
index 8c407b867fe..a7605cd483a 100644
--- a/spec/support/fake_u2f_device.rb
+++ b/spec/support/fake_u2f_device.rb
@@ -5,7 +5,7 @@ class FakeU2fDevice
@page = page
@name = name
end
-
+
def respond_to_u2f_registration
app_id = @page.evaluate_script('gon.u2f.app_id')
challenges = @page.evaluate_script('gon.u2f.challenges')
@@ -28,6 +28,7 @@ class FakeU2fDevice
u2f.sign = function(appId, challenges, signRequests, callback) {
callback(#{json_response});
};
+ window.gl.u2fAuthenticate.start();
")
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 5e3b8f2b23e..a4713e53f63 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -76,7 +76,7 @@ shared_examples 'issuable record that supports slash commands in its description
expect(page).not_to have_content '/assign @bob'
expect(page).not_to have_content '/label ~bug'
expect(page).not_to have_content '/milestone %"ASAP"'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
issuable.reload
@@ -97,7 +97,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/close")
expect(page).not_to have_content '/close'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(issuable.reload).to be_closed
end
@@ -114,7 +114,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/close")
expect(page).not_to have_content '/close'
- expect(page).not_to have_content 'Your commands have been executed!'
+ expect(page).not_to have_content 'Commands applied'
expect(issuable).to be_open
end
@@ -132,7 +132,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/reopen")
expect(page).not_to have_content '/reopen'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(issuable.reload).to be_open
end
@@ -149,7 +149,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/reopen")
expect(page).not_to have_content '/reopen'
- expect(page).not_to have_content 'Your commands have been executed!'
+ expect(page).not_to have_content 'Commands applied'
expect(issuable).to be_closed
end
@@ -162,7 +162,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(issuable.reload.title).to eq 'Awesome new title'
end
@@ -179,7 +179,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
- expect(page).not_to have_content 'Your commands have been executed!'
+ expect(page).not_to have_content 'Commands applied'
expect(issuable.reload.title).not_to eq 'Awesome new title'
end
@@ -191,7 +191,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/todo")
expect(page).not_to have_content '/todo'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
todos = TodosFinder.new(master).execute
todo = todos.first
@@ -222,7 +222,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/done")
expect(page).not_to have_content '/done'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
expect(todo.reload).to be_done
end
@@ -230,31 +230,31 @@ shared_examples 'issuable record that supports slash commands in its description
context "with a note subscribing to the #{issuable_type}" do
it "creates a new todo for the #{issuable_type}" do
- expect(issuable.subscribed?(master)).to be_falsy
+ expect(issuable.subscribed?(master, project)).to be_falsy
write_note("/subscribe")
expect(page).not_to have_content '/subscribe'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
- expect(issuable.subscribed?(master)).to be_truthy
+ expect(issuable.subscribed?(master, project)).to be_truthy
end
end
context "with a note unsubscribing to the #{issuable_type} as done" do
before do
- issuable.subscribe(master)
+ issuable.subscribe(master, project)
end
it "creates a new todo for the #{issuable_type}" do
- expect(issuable.subscribed?(master)).to be_truthy
+ expect(issuable.subscribed?(master, project)).to be_truthy
write_note("/unsubscribe")
expect(page).not_to have_content '/unsubscribe'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).to have_content 'Commands applied'
- expect(issuable.subscribed?(master)).to be_falsy
+ expect(issuable.subscribed?(master, project)).to be_falsy
end
end
end
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
new file mode 100644
index 00000000000..2542a59bb00
--- /dev/null
+++ b/spec/support/import_export/common_util.rb
@@ -0,0 +1,10 @@
+module ImportExport
+ module CommonUtil
+ def setup_symlink(tmpdir, symlink_name)
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(tmpdir)
+
+ File.open("#{tmpdir}/test", 'w') { |file| file.write("test") }
+ FileUtils.ln_s("#{tmpdir}/test", "#{tmpdir}/#{symlink_name}")
+ end
+ end
+end
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
index adc3f48b434..0b8729db0f9 100644
--- a/spec/support/javascript_fixtures_helpers.rb
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -1,3 +1,4 @@
+require 'action_dispatch/testing/test_request'
require 'fileutils'
require 'gitlab/popen'
@@ -19,27 +20,44 @@ module JavaScriptFixturesHelpers
# Public: Store a response object as fixture file
#
- # response - response object to store
+ # response - string or response object to store
# fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH)
#
def store_frontend_fixture(response, fixture_file_name)
fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH)
+ fixture = response.respond_to?(:body) ? parse_response(response) : response
+
+ FileUtils.mkdir_p(File.dirname(fixture_file_name))
+ File.write(fixture_file_name, fixture)
+ end
+
+ private
+
+ # Private: Prepare a response object for use as a frontend fixture
+ #
+ # response - response object to prepare
+ #
+ def parse_response(response)
fixture = response.body
+ fixture.force_encoding("utf-8")
response_mime_type = Mime::Type.lookup(response.content_type)
if response_mime_type.html?
doc = Nokogiri::HTML::DocumentFragment.parse(fixture)
- scripts = doc.css('script')
+ link_tags = doc.css('link')
+ link_tags.remove
+
+ scripts = doc.css("script:not([type='text/template'])")
scripts.remove
fixture = doc.to_html
# replace relative links
- fixture.gsub!(%r{="/}, '="https://fixture.invalid/')
+ test_host = ActionDispatch::TestRequest::DEFAULT_ENV['HTTP_HOST']
+ fixture.gsub!(%r{="/}, "=\"http://#{test_host}/")
end
- FileUtils.mkdir_p(File.dirname(fixture_file_name))
- File.write(fixture_file_name, fixture)
+ fixture
end
end
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index 96e0dad6b55..929fc0c5182 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -6,7 +6,8 @@ module JiraServiceHelper
properties = {
title: "JIRA tracker",
url: JIRA_URL,
- project_key: "JIRA"
+ project_key: "JIRA",
+ jira_issue_transition_id: '1'
}
jira_tracker.update_attributes(properties: properties, active: true)
@@ -57,6 +58,10 @@ module JiraServiceHelper
JIRA_API + "/issue/#{issue_id}/comment"
end
+ def jira_api_remote_link_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}/remotelink"
+ end
+
def jira_api_transition_url(issue_id)
JIRA_API + "/issue/#{issue_id}/transitions"
end
@@ -75,6 +80,7 @@ module JiraServiceHelper
WebMock.stub_request(:get, jira_issue_url(issue_id))
WebMock.stub_request(:get, jira_api_test_url)
WebMock.stub_request(:post, jira_api_comment_url(issue_id))
+ WebMock.stub_request(:post, jira_api_remote_link_url(issue_id))
WebMock.stub_request(:post, jira_api_transition_url(issue_id))
end
end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
new file mode 100644
index 00000000000..6c4c246a68b
--- /dev/null
+++ b/spec/support/kubernetes_helpers.rb
@@ -0,0 +1,52 @@
+module KubernetesHelpers
+ include Gitlab::Kubernetes
+
+ def kube_discovery_body
+ { "kind" => "APIResourceList",
+ "resources" => [
+ { "name" => "pods", "namespaced" => true, "kind" => "Pod" },
+ ],
+ }
+ end
+
+ def kube_pods_body(*pods)
+ { "kind" => "PodList",
+ "items" => [ kube_pod ],
+ }
+ end
+
+ # This is a partial response, it will have many more elements in reality but
+ # these are the ones we care about at the moment
+ def kube_pod(app: "valid-pod-label")
+ { "metadata" => {
+ "name" => "kube-pod",
+ "creationTimestamp" => "2016-11-25T19:55:19Z",
+ "labels" => { "app" => app },
+ },
+ "spec" => {
+ "containers" => [
+ { "name" => "container-0" },
+ { "name" => "container-1" },
+ ],
+ },
+ "status" => { "phase" => "Running" },
+ }
+ end
+
+ def kube_terminals(service, pod)
+ pod_name = pod['metadata']['name']
+ containers = pod['spec']['containers']
+
+ containers.map do |container|
+ terminal = {
+ selectors: { pod: pod_name, container: container['name'] },
+ url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']),
+ subprotocols: ['channel.k8s.io'],
+ headers: { 'Authorization' => ["Bearer #{service.token}"] },
+ created_at: DateTime.parse(pod['metadata']['creationTimestamp'])
+ }
+ terminal[:ca_pem] = service.ca_pem if service.ca_pem.present?
+ terminal
+ end
+ end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index c0b3e83244d..ad1eed5b369 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -75,7 +75,8 @@ module LoginHelpers
def logout
find(".header-user-dropdown-toggle").click
click_link "Sign out"
- expect(page).to have_content('Signed out successfully')
+ # check the sign_in button
+ expect(page).to have_button('Sign in')
end
# Logout without JavaScript driver
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index 0497e391860..ceddb656596 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -7,7 +7,7 @@ module AccessMatchers
extend RSpec::Matchers::DSL
include Warden::Test::Helpers
- def emulate_user(user)
+ def emulate_user(user, membership = nil)
case user
when :user
login_as(create(:user))
@@ -19,6 +19,19 @@ module AccessMatchers
login_as(create(:user, external: true))
when User
login_as(user)
+ when *Gitlab::Access.sym_options_with_owner.keys
+ raise ArgumentError, "cannot emulate #{user} without membership parent" unless membership
+
+ role = user
+
+ if role == :owner && membership.owner
+ user = membership.owner
+ else
+ user = create(:user)
+ membership.public_send(:"add_#{role}", user)
+ end
+
+ login_as(user)
else
raise ArgumentError, "cannot emulate user #{user}"
end
@@ -26,8 +39,7 @@ module AccessMatchers
def description_for(user, type)
if user.kind_of?(User)
- # User#inspect displays too much information for RSpec's description
- # messages
+ # User#inspect displays too much information for RSpec's descriptions
"be #{type} for the specified user"
else
"be #{type} for #{user}"
@@ -36,21 +48,31 @@ module AccessMatchers
matcher :be_allowed_for do |user|
match do |url|
- emulate_user(user)
- visit url
+ emulate_user(user, @membership)
+ visit(url)
+
status_code != 404 && current_path != new_user_session_path
end
+ chain :of do |membership|
+ @membership = membership
+ end
+
description { description_for(user, 'allowed') }
end
matcher :be_denied_for do |user|
match do |url|
- emulate_user(user)
- visit url
+ emulate_user(user, @membership)
+ visit(url)
+
status_code == 404 || current_path == new_user_session_path
end
+ chain :of do |membership|
+ @membership = membership
+ end
+
description { description_for(user, 'denied') }
end
end
diff --git a/spec/support/matchers/be_url.rb b/spec/support/matchers/be_url.rb
new file mode 100644
index 00000000000..f8096af1b22
--- /dev/null
+++ b/spec/support/matchers/be_url.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :be_url do |_|
+ match do |actual|
+ URI.parse(actual) rescue false
+ end
+end
diff --git a/spec/support/matchers/be_valid_commit.rb b/spec/support/matchers/be_valid_commit.rb
new file mode 100644
index 00000000000..3696e4d5f03
--- /dev/null
+++ b/spec/support/matchers/be_valid_commit.rb
@@ -0,0 +1,8 @@
+RSpec::Matchers.define :be_valid_commit do
+ match do |actual|
+ actual &&
+ actual.id == SeedRepo::Commit::ID &&
+ actual.message == SeedRepo::Commit::MESSAGE &&
+ actual.author_name == SeedRepo::Commit::AUTHOR_FULL_NAME
+ end
+end
diff --git a/spec/support/matchers/have_issuable_counts.rb b/spec/support/matchers/have_issuable_counts.rb
index 02605d6b70e..92cf3de5448 100644
--- a/spec/support/matchers/have_issuable_counts.rb
+++ b/spec/support/matchers/have_issuable_counts.rb
@@ -1,9 +1,9 @@
RSpec::Matchers.define :have_issuable_counts do |opts|
- match do |actual|
- expected_counts = opts.map do |state, count|
- "#{state.to_s.humanize} #{count}"
- end
+ expected_counts = opts.map do |state, count|
+ "#{state.to_s.humanize} #{count}"
+ end
+ match do |actual|
actual.within '.issues-state-filters' do
expected_counts.each do |expected_count|
expect(actual).to have_content(expected_count)
diff --git a/spec/support/matchers/is_within.rb b/spec/support/matchers/is_within.rb
deleted file mode 100644
index 0c35fc7e899..00000000000
--- a/spec/support/matchers/is_within.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# Extend shoulda-matchers
-module Shoulda::Matchers::ActiveModel
- class ValidateLengthOfMatcher
- # Shortcut for is_at_least and is_at_most
- def is_within(range)
- is_at_least(range.min) && is_at_most(range.max)
- end
- end
-end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 8c98b1f988c..97b8b342eb2 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -38,9 +38,9 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('h1 a#gitlab-markdown')
- expect(actual).to have_selector('h2 a#markdown')
- expect(actual).to have_selector('h3 a#autolinkfilter')
+ expect(actual).to have_selector('h1 a#user-content-gitlab-markdown')
+ expect(actual).to have_selector('h2 a#user-content-markdown')
+ expect(actual).to have_selector('h3 a#user-content-autolinkfilter')
end
end
diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb
index 3956d05060b..a3724b801b3 100644
--- a/spec/support/notify_shared_examples.rb
+++ b/spec/support/notify_shared_examples.rb
@@ -7,7 +7,7 @@ shared_context 'gitlab email notification' do
let(:new_user_address) { 'newguy@example.com' }
before do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
email = recipient.emails.create(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email)
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
@@ -179,9 +179,24 @@ shared_examples 'it should show Gmail Actions View Commit link' do
end
shared_examples 'an unsubscribeable thread' do
+ it_behaves_like 'an unsubscribeable thread with incoming address without %{key}'
+
+ it 'has a List-Unsubscribe header in the correct format' do
+ is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
+ is_expected.to have_header 'List-Unsubscribe', /mailto/
+ is_expected.to have_header 'List-Unsubscribe', /^<.+,.+>$/
+ end
+
+ it { is_expected.to have_body_text /unsubscribe/ }
+end
+
+shared_examples 'an unsubscribeable thread with incoming address without %{key}' do
+ include_context 'reply-by-email is enabled with incoming address without %{key}'
+
it 'has a List-Unsubscribe header in the correct format' do
is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
- is_expected.to have_header 'List-Unsubscribe', /^<.+>$/
+ is_expected.not_to have_header 'List-Unsubscribe', /mailto/
+ is_expected.to have_header 'List-Unsubscribe', /^<[^,]+>$/
end
it { is_expected.to have_body_text /unsubscribe/ }
diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/project_features_apply_to_issuables_shared_examples.rb
new file mode 100644
index 00000000000..4621d17549b
--- /dev/null
+++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb
@@ -0,0 +1,56 @@
+shared_examples 'project features apply to issuables' do |klass|
+ let(:described_class) { klass }
+
+ let(:group) { create(:group) }
+ let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user }
+ let(:user_outside_group) { create(:user) }
+
+ let(:project) { create(:empty_project, :public, project_args) }
+
+ def project_args
+ feature = "#{described_class.model_name.plural}_access_level".to_sym
+
+ args = { group: group }
+ args[feature] = access_level
+
+ args
+ end
+
+ before do
+ _ = issuable
+ login_as(user)
+ visit path
+ end
+
+ context 'public access level' do
+ let(:access_level) { ProjectFeature::ENABLED }
+
+ context 'group member' do
+ let(:user) { user_in_group }
+
+ it { expect(page).to have_content(issuable.title) }
+ end
+
+ context 'non-member' do
+ let(:user) { user_outside_group }
+
+ it { expect(page).to have_content(issuable.title) }
+ end
+ end
+
+ context 'private access level' do
+ let(:access_level) { ProjectFeature::PRIVATE }
+
+ context 'group member' do
+ let(:user) { user_in_group }
+
+ it { expect(page).to have_content(issuable.title) }
+ end
+
+ context 'non-member' do
+ let(:user) { user_outside_group }
+
+ it { expect(page).not_to have_content(issuable.title) }
+ end
+ end
+end
diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb
new file mode 100644
index 00000000000..e40d5ebd9a8
--- /dev/null
+++ b/spec/support/query_recorder.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ class QueryRecorder
+ attr_reader :log
+
+ def initialize(&block)
+ @log = []
+ ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
+ end
+
+ def callback(name, start, finish, message_id, values)
+ return if %w(CACHE SCHEMA).include?(values[:name])
+ @log << values[:sql]
+ end
+
+ def count
+ @log.count
+ end
+
+ def log_message
+ @log.join("\n\n")
+ end
+ end
+end
+
+RSpec::Matchers.define :exceed_query_limit do |expected|
+ supports_block_expectations
+
+ match do |block|
+ query_count(&block) > expected
+ end
+
+ failure_message_when_negated do |actual|
+ "Expected a maximum of #{expected} queries, got #{@recorder.count}:\n\n#{@recorder.log_message}"
+ end
+
+ def query_count(&block)
+ @recorder = ActiveRecord::QueryRecorder.new(&block)
+ @recorder.count
+ end
+end
diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb
index 52d80c69835..4a8158ed79b 100644
--- a/spec/support/rake_helpers.rb
+++ b/spec/support/rake_helpers.rb
@@ -1,7 +1,7 @@
module RakeHelpers
- def run_rake_task(task_name)
+ def run_rake_task(task_name, *args)
Rake::Task[task_name].reenable
- Rake.application.invoke_task task_name
+ Rake.application.invoke_task("#{task_name}[#{args.join(',')}]")
end
def stub_warn_user_is_not_gitlab
diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb
new file mode 100644
index 00000000000..98eb57f8b54
--- /dev/null
+++ b/spec/support/reactive_caching_helpers.rb
@@ -0,0 +1,42 @@
+module ReactiveCachingHelpers
+ def reactive_cache_key(subject, *qualifiers)
+ ([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':')
+ end
+
+ def alive_reactive_cache_key(subject, *qualifiers)
+ reactive_cache_key(subject, *(qualifiers + ['alive']))
+ end
+
+ def stub_reactive_cache(subject = nil, data = nil, *qualifiers)
+ allow(ReactiveCachingWorker).to receive(:perform_async)
+ allow(ReactiveCachingWorker).to receive(:perform_in)
+ write_reactive_cache(subject, data, *qualifiers) if data
+ end
+
+ def read_reactive_cache(subject, *qualifiers)
+ Rails.cache.read(reactive_cache_key(subject, *qualifiers))
+ end
+
+ def write_reactive_cache(subject, data, *qualifiers)
+ start_reactive_cache_lifetime(subject, *qualifiers)
+ Rails.cache.write(reactive_cache_key(subject, *qualifiers), data)
+ end
+
+ def reactive_cache_alive?(subject, *qualifiers)
+ Rails.cache.read(alive_reactive_cache_key(subject, *qualifiers))
+ end
+
+ def invalidate_reactive_cache(subject, *qualifiers)
+ Rails.cache.delete(alive_reactive_cache_key(subject, *qualifiers))
+ end
+
+ def start_reactive_cache_lifetime(subject, *qualifiers)
+ Rails.cache.write(alive_reactive_cache_key(subject, *qualifiers), true)
+ end
+
+ def expect_reactive_cache_update_queued(subject)
+ expect(ReactiveCachingWorker).
+ to receive(:perform_in).
+ with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id)
+ end
+end
diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/reference_parser_shared_examples.rb
new file mode 100644
index 00000000000..8eb74635a60
--- /dev/null
+++ b/spec/support/reference_parser_shared_examples.rb
@@ -0,0 +1,43 @@
+RSpec.shared_examples "referenced feature visibility" do |*related_features|
+ let(:feature_fields) do
+ related_features.map { |feature| (feature + "_access_level").to_sym }
+ end
+
+ before { link['data-project'] = project.id.to_s }
+
+ context "when feature is disabled" do
+ it "does not create reference" do
+ set_features_fields_to(ProjectFeature::DISABLED)
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context "when feature is enabled only for team members" do
+ before { set_features_fields_to(ProjectFeature::PRIVATE) }
+
+ it "does not create reference for non member" do
+ non_member = create(:user)
+
+ expect(subject.nodes_visible_to_user(non_member, [link])).to eq([])
+ end
+
+ it "creates reference for member" do
+ project.team << [user, :developer]
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+
+ context "when feature is enabled" do
+ # The project is public
+ it "creates reference" do
+ set_features_fields_to(ProjectFeature::ENABLED)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+
+ def set_features_fields_to(visibility_level)
+ feature_fields.each { |field| project.project_feature.update_attribute(field, visibility_level) }
+ end
+end
diff --git a/spec/support/search_helpers.rb b/spec/support/search_helpers.rb
new file mode 100644
index 00000000000..abbbb636d66
--- /dev/null
+++ b/spec/support/search_helpers.rb
@@ -0,0 +1,5 @@
+module SearchHelpers
+ def select_filter(name)
+ find(:xpath, "//ul[contains(@class, 'search-filter')]//a[contains(.,'#{name}')]").click
+ end
+end
diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb
new file mode 100644
index 00000000000..03fa0a66b9a
--- /dev/null
+++ b/spec/support/seed_helper.rb
@@ -0,0 +1,112 @@
+# This file is specific to specs in spec/lib/gitlab/git/
+
+SEED_REPOSITORY_PATH = File.expand_path('../../tmp/repositories', __dir__)
+TEST_REPO_PATH = File.join(SEED_REPOSITORY_PATH, 'gitlab-git-test.git')
+TEST_NORMAL_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "not-bare-repo.git")
+TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git")
+TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git")
+
+module SeedHelper
+ GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git"
+
+ def ensure_seeds
+ if File.exist?(SEED_REPOSITORY_PATH)
+ FileUtils.rm_r(SEED_REPOSITORY_PATH)
+ end
+
+ FileUtils.mkdir_p(SEED_REPOSITORY_PATH)
+
+ create_bare_seeds
+ create_normal_seeds
+ create_mutable_seeds
+ create_broken_seeds
+ create_git_attributes
+ create_invalid_git_attributes
+ end
+
+ def create_bare_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}),
+ chdir: SEED_REPOSITORY_PATH,
+ out: '/dev/null',
+ err: '/dev/null')
+ end
+
+ def create_normal_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}),
+ out: '/dev/null',
+ err: '/dev/null')
+ end
+
+ def create_mutable_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
+ out: '/dev/null',
+ err: '/dev/null')
+
+ system(git_env, *%w(git branch -t feature origin/feature),
+ chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
+
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}),
+ chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
+ end
+
+ def create_broken_seeds
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}),
+ out: '/dev/null',
+ err: '/dev/null')
+
+ refs_path = File.join(TEST_BROKEN_REPO_PATH, 'refs')
+
+ FileUtils.rm_r(refs_path)
+ end
+
+ def create_git_attributes
+ dir = File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git', 'info')
+
+ FileUtils.mkdir_p(dir)
+
+ File.open(File.join(dir, 'attributes'), 'w') do |handle|
+ handle.write <<-EOF.strip
+# This is a comment, it should be ignored.
+
+*.txt text
+*.jpg -text
+*.sh eol=lf gitlab-language=shell
+*.haml.* gitlab-language=haml
+foo/bar.* foo
+*.cgi key=value?p1=v1&p2=v2
+/*.png gitlab-language=png
+*.binary binary
+
+# This uses a tab instead of spaces to ensure the parser also supports this.
+*.md\tgitlab-language=markdown
+bla/bla.txt
+ EOF
+ end
+ end
+
+ def create_invalid_git_attributes
+ dir = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git', 'info')
+
+ FileUtils.mkdir_p(dir)
+
+ enc = Encoding::UTF_16
+
+ File.open(File.join(dir, 'attributes'), 'w', encoding: enc) do |handle|
+ handle.write('# hello'.encode(enc))
+ end
+ end
+
+ # Prevent developer git configurations from being persisted to test
+ # repositories
+ def git_env
+ { 'GIT_TEMPLATE_DIR' => '' }
+ end
+end
+
+RSpec.configure do |config|
+ config.include SeedHelper, :seed_helper
+
+ config.before(:all, :seed_helper) do
+ ensure_seeds
+ end
+end
diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb
new file mode 100644
index 00000000000..9f2cd7c67c5
--- /dev/null
+++ b/spec/support/seed_repo.rb
@@ -0,0 +1,143 @@
+# Seed repo:
+# 0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326 Merge branch 'lfs_pointers' into 'master'
+# 33bcff41c232a11727ac6d660bd4b0c2ba86d63d Add valid and invalid lfs pointers
+# 732401c65e924df81435deb12891ef570167d2e2 Update year in license file
+# b0e52af38d7ea43cf41d8a6f2471351ac036d6c9 Empty commit
+# 40f4a7a617393735a95a0bb67b08385bc1e7c66d Add ISO-8859-encoded file
+# 66028349a123e695b589e09a36634d976edcc5e8 Merge branch 'add-comments-to-gitmodules' into 'master'
+# de5714f34c4e34f1d50b9a61a2e6c9132fe2b5fd Add comments to the end of .gitmodules to test parsing
+# fa1b1e6c004a68b7d8763b86455da9e6b23e36d6 Merge branch 'add-files' into 'master'
+# eb49186cfa5c4338011f5f590fac11bd66c5c631 Add submodules nested deeper than the root
+# 18d9c205d0d22fdf62bc2f899443b83aafbf941f Add executables and links files
+# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
+# 570e7b2abdd848b95f2f578043fc23bd6f6fd24d Change some files
+# 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 More submodules
+# d14d6c0abdd253381df51a723d58691b2ee1ab08 Remove ds_store files
+# c1acaa58bbcbc3eafe538cb8274ba387047b69f8 Ignore DS files
+# ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added
+# 874797c3a73b60d2187ed6e2fcabd289ff75171e Ruby files modified
+# 2f63565e7aac07bcdadb654e253078b727143ec4 Modified image
+# 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added
+# 913c66a37b4a45b9769037c55c2d238bd0942d2e Files, encoding and much more
+# cfe32cf61b73a0d5e9f13e774abde7ff789b1660 Add submodule
+# 6d394385cf567f80a8fd85055db1ab4c5295806f Added contributing guide
+# 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 Initial commit
+
+module SeedRepo
+ module BigCommit
+ ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e"
+ PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660"
+ MESSAGE = "Files, encoding and much more"
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
+ FILES_COUNT = 2
+ end
+
+ module Commit
+ ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
+ PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ MESSAGE = "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
+ FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"]
+ FILES_COUNT = 2
+ C_FILE_PATH = "files/ruby"
+ C_FILES = ["popen.rb", "regex.rb", "version_info.rb"]
+ BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n}
+ BLOB_FILE_PATH = "app/views/keys/show.html.haml"
+ end
+
+ module EmptyCommit
+ ID = "b0e52af38d7ea43cf41d8a6f2471351ac036d6c9"
+ PARENT_ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
+ MESSAGE = "Empty commit"
+ AUTHOR_FULL_NAME = "Rémy Coutable"
+ FILES = []
+ FILES_COUNT = FILES.count
+ end
+
+ module EncodingCommit
+ ID = "40f4a7a617393735a95a0bb67b08385bc1e7c66d"
+ PARENT_ID = "66028349a123e695b589e09a36634d976edcc5e8"
+ MESSAGE = "Add ISO-8859-encoded file"
+ AUTHOR_FULL_NAME = "Stan Hu"
+ FILES = ["encoding/iso8859.txt"]
+ FILES_COUNT = FILES.count
+ end
+
+ module FirstCommit
+ ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863"
+ PARENT_ID = nil
+ MESSAGE = "Initial commit"
+ AUTHOR_FULL_NAME = "Dmitriy Zaporozhets"
+ FILES = ["LICENSE", ".gitignore", "README.md"]
+ FILES_COUNT = 3
+ end
+
+ module LastCommit
+ ID = "4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6"
+ PARENT_ID = "0e1b353b348f8477bdbec1ef47087171c5032cd9"
+ MESSAGE = "Merge branch 'master' into 'master'"
+ AUTHOR_FULL_NAME = "Stan Hu"
+ FILES = ["bin/executable"]
+ FILES_COUNT = FILES.count
+ end
+
+ module Repo
+ HEAD = "master"
+ BRANCHES = %w[
+ feature
+ fix
+ fix-blob-path
+ fix-existing-submodule-dir
+ fix-mode
+ gitattributes
+ gitattributes-updated
+ master
+ merge-test
+ ]
+ TAGS = %w[v1.0.0 v1.1.0 v1.2.0 v1.2.1]
+ end
+
+ module RubyBlob
+ ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c"
+ NAME = "popen.rb"
+ CONTENT = <<-eos
+require 'fileutils'
+require 'open3'
+
+module Popen
+ extend self
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+
+ path ||= Dir.pwd
+
+ vars = {
+ "PWD" => path
+ }
+
+ options = {
+ chdir: path
+ }
+
+ unless File.directory?(path)
+ FileUtils.mkdir_p(path)
+ end
+
+ @cmd_output = ""
+ @cmd_status = 0
+
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+ @cmd_status = wait_thr.value.exitstatus
+ end
+
+ return @cmd_output, @cmd_status
+ end
+end
+ eos
+ end
+end
diff --git a/spec/support/services/issuable_create_service_shared_examples.rb b/spec/support/services/issuable_create_service_shared_examples.rb
new file mode 100644
index 00000000000..93c0267d2db
--- /dev/null
+++ b/spec/support/services/issuable_create_service_shared_examples.rb
@@ -0,0 +1,52 @@
+shared_examples 'issuable create service' do
+ context 'asssignee_id' do
+ let(:assignee) { create(:user) }
+
+ before { project.team << [user, :master] }
+
+ it 'removes assignee_id when user id is invalid' do
+ opts = { title: 'Title', description: 'Description', assignee_id: -1 }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to be_nil
+ end
+
+ it 'removes assignee_id when user id is 0' do
+ opts = { title: 'Title', description: 'Description', assignee_id: 0 }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to be_nil
+ end
+
+ it 'saves assignee when user id is valid' do
+ project.team << [assignee, :master]
+ opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to eq(assignee.id)
+ end
+
+ context "when issuable feature is private" do
+ before do
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
+
+ levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+ levels.each do |level|
+ it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+ project.update(visibility_level: level)
+ opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+
+ issuable = described_class.new(project, user, opts).execute
+
+ expect(issuable.assignee_id).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index 5f9645ed44f..dd54b0addda 100644
--- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -11,6 +11,8 @@ shared_examples 'new issuable record that supports slash commands' do
let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) }
let(:issuable) { described_class.new(project, user, params).execute }
+ before { project.team << [assignee, :master ] }
+
context 'with labels in command only' do
let(:example_params) do
{
@@ -55,7 +57,7 @@ shared_examples 'new issuable record that supports slash commands' do
context 'with assignee and milestone in params and command' do
let(:example_params) do
{
- assignee: build_stubbed(:user),
+ assignee: create(:user),
milestone_id: double(:milestone),
description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
}
diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb
new file mode 100644
index 00000000000..49cea1e608c
--- /dev/null
+++ b/spec/support/services/issuable_update_service_shared_examples.rb
@@ -0,0 +1,69 @@
+shared_examples 'issuable update service' do
+ def update_issuable(opts)
+ described_class.new(project, user, opts).execute(open_issuable)
+ end
+
+ context 'changing state' do
+ before { expect(project).to receive(:execute_hooks).once }
+
+ context 'to reopened' do
+ it 'executes hooks only once' do
+ described_class.new(project, user, state_event: 'reopen').execute(closed_issuable)
+ end
+ end
+
+ context 'to closed' do
+ it 'executes hooks only once' do
+ described_class.new(project, user, state_event: 'close').execute(open_issuable)
+ end
+ end
+ end
+
+ context 'asssignee_id' do
+ it 'does not update assignee when assignee_id is invalid' do
+ open_issuable.update(assignee_id: user.id)
+
+ update_issuable(assignee_id: -1)
+
+ expect(open_issuable.reload.assignee).to eq(user)
+ end
+
+ it 'unassigns assignee when user id is 0' do
+ open_issuable.update(assignee_id: user.id)
+
+ update_issuable(assignee_id: 0)
+
+ expect(open_issuable.assignee_id).to be_nil
+ end
+
+ it 'saves assignee when user id is valid' do
+ update_issuable(assignee_id: user.id)
+
+ expect(open_issuable.assignee_id).to eq(user.id)
+ end
+
+ it 'does not update assignee_id when user cannot read issue' do
+ non_member = create(:user)
+ original_assignee = open_issuable.assignee
+
+ update_issuable(assignee_id: non_member.id)
+
+ expect(open_issuable.assignee_id).to eq(original_assignee.id)
+ end
+
+ context "when issuable feature is private" do
+ levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
+
+ levels.each do |level|
+ it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
+ assignee = create(:user)
+ project.update(visibility_level: level)
+ feature_visibility_attr = :"#{open_issuable.model_name.plural}_access_level"
+ project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
+
+ expect{ update_issuable(assignee_id: assignee) }.not_to change{ open_issuable.assignee }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb
index d1c999cad4d..66c93890e31 100644
--- a/spec/support/services_shared_context.rb
+++ b/spec/support/services_shared_context.rb
@@ -16,8 +16,14 @@ Service.available_services_names.each do |service|
hash.merge!(k => 'secrettoken')
elsif k =~ /^(.*_url|url|webhook)/
hash.merge!(k => "http://example.com")
+ elsif service_klass.method_defined?("#{k}?")
+ hash.merge!(k => true)
elsif service == 'irker' && k == :recipients
hash.merge!(k => 'irc://irc.network.net:666/#channel')
+ elsif service == 'irker' && k == :server_port
+ hash.merge!(k => 1234)
+ elsif service == 'jira' && k == :jira_issue_transition_id
+ hash.merge!(k => 1234)
else
hash.merge!(k => "someword")
end
diff --git a/spec/support/setup_builds_storage.rb b/spec/support/setup_builds_storage.rb
index a4f21e95338..2e7c88bfc09 100644
--- a/spec/support/setup_builds_storage.rb
+++ b/spec/support/setup_builds_storage.rb
@@ -1,19 +1,18 @@
RSpec.configure do |config|
def builds_path
- Rails.root.join('tmp/builds')
+ Rails.root.join('tmp/tests/builds')
end
- config.before(:each) do
- FileUtils.mkdir_p(builds_path)
- FileUtils.touch(File.join(builds_path, ".gitkeep"))
+ config.before(:suite) do
Settings.gitlab_ci['builds_path'] = builds_path
end
- config.after(:suite) do
- Dir[File.join(builds_path, '*')].each do |path|
- next if File.basename(path) == '.gitkeep'
+ config.before(:all) do
+ FileUtils.mkdir_p(builds_path)
+ end
- FileUtils.rm_rf(path)
- end
+ config.before(:each) do
+ FileUtils.rm_rf(builds_path)
+ FileUtils.mkdir_p(builds_path)
end
end
diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb
new file mode 100644
index 00000000000..74d9b8c6313
--- /dev/null
+++ b/spec/support/slack_mattermost_notifications_shared_examples.rb
@@ -0,0 +1,328 @@
+Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
+
+RSpec.shared_examples 'slack or mattermost notifications' do
+ let(:chat_service) { described_class.new }
+ let(:webhook_url) { 'https://example.gitlab.com/' }
+
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before { subject.active = true }
+
+ it { is_expected.to validate_presence_of(:webhook) }
+ it_behaves_like 'issue tracker service URL attribute', :webhook
+ end
+
+ context 'when service is inactive' do
+ before { subject.active = false }
+
+ it { is_expected.not_to validate_presence_of(:webhook) }
+ end
+ end
+
+ describe "#execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:username) { 'slack_username' }
+ let(:channel) { 'slack_channel' }
+
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+
+ opts = {
+ title: 'Awesome issue',
+ description: 'please fix'
+ }
+
+ issue_service = Issues::CreateService.new(project, user, opts)
+ @issue = issue_service.execute
+ @issues_sample_data = issue_service.hook_data(@issue, 'open')
+
+ opts = {
+ title: 'Awesome merge_request',
+ description: 'please fix',
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ merge_service = MergeRequests::CreateService.new(project,
+ user, opts)
+ @merge_request = merge_service.execute
+ @merge_sample_data = merge_service.hook_data(@merge_request,
+ 'open')
+
+ opts = {
+ title: "Awesome wiki_page",
+ content: "Some text describing some thing or another",
+ format: "md",
+ message: "user created page: Awesome wiki_page"
+ }
+
+ wiki_page_service = WikiPages::CreateService.new(project, user, opts)
+ @wiki_page = wiki_page_service.execute
+ @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
+ end
+
+ it "calls Slack/Mattermost API for push events" do
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it "calls Slack/Mattermost API for issue events" do
+ chat_service.execute(@issues_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it "calls Slack/Mattermost API for merge requests events" do
+ chat_service.execute(@merge_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it "calls Slack/Mattermost API for wiki page events" do
+ chat_service.execute(@wiki_page_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it 'uses the username as an option for slack when configured' do
+ allow(chat_service).to receive(:username).and_return(username)
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, username: username).
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(push_sample_data)
+ end
+
+ it 'uses the channel as an option when it is configured' do
+ allow(chat_service).to receive(:channel).and_return(channel)
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: channel).
+ and_return(
+ double(:slack_service).as_null_object
+ )
+ chat_service.execute(push_sample_data)
+ end
+
+ context "event channels" do
+ it "uses the right channel for push event" do
+ chat_service.update_attributes(push_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(push_sample_data)
+ end
+
+ it "uses the right channel for merge request event" do
+ chat_service.update_attributes(merge_request_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(@merge_sample_data)
+ end
+
+ it "uses the right channel for issue event" do
+ chat_service.update_attributes(issue_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(@issues_sample_data)
+ end
+
+ it "uses the right channel for wiki event" do
+ chat_service.update_attributes(wiki_page_channel: "random")
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(@wiki_page_sample_data)
+ end
+
+ context "note event" do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
+
+ it "uses the right channel" do
+ chat_service.update_attributes(note_channel: "random")
+
+ note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
+
+ expect(Slack::Notifier).to receive(:new).
+ with(webhook_url, channel: "random").
+ and_return(
+ double(:slack_service).as_null_object
+ )
+
+ chat_service.execute(note_data)
+ end
+ end
+ end
+ end
+
+ describe "Note events" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, creator_id: user.id) }
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ context 'when commit comment event executed' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
+
+ it "calls Slack/Mattermost API for commit comment events" do
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when merge request comment event executed' do
+ let(:merge_request_note) do
+ create(:note_on_merge_request, project: project,
+ note: "merge request note")
+ end
+
+ it "calls Slack API for merge request comment events" do
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when issue comment event executed' do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
+
+ it "calls Slack API for issue comment events" do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when snippet comment event executed' do
+ let(:snippet_note) do
+ create(:note_on_project_snippet, project: project,
+ note: "snippet note")
+ end
+
+ it "calls Slack API for snippet comment events" do
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+ end
+
+ describe 'Pipeline events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project, status: status,
+ sha: project.commit.sha, ref: project.default_branch)
+ end
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ service_hook: true,
+ webhook: webhook_url
+ )
+ end
+
+ shared_examples 'call Slack/Mattermost API' do
+ before do
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ it 'calls Slack/Mattermost API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'with failed pipeline' do
+ let(:status) { 'failed' }
+
+ it_behaves_like 'call Slack/Mattermost API'
+ end
+
+ context 'with succeeded pipeline' do
+ let(:status) { 'success' }
+
+ context 'with default to notify_only_broken_pipelines' do
+ it 'does not call Slack/Mattermost API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ result = chat_service.execute(data)
+
+ expect(result).to be_falsy
+ end
+ end
+
+ context 'with setting notify_only_broken_pipelines to false' do
+ before do
+ chat_service.notify_only_broken_pipelines = false
+ end
+
+ it_behaves_like 'call Slack/Mattermost API'
+ end
+ end
+ end
+end
diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb
new file mode 100644
index 00000000000..18597b5c71f
--- /dev/null
+++ b/spec/support/stub_env.rb
@@ -0,0 +1,7 @@
+module StubENV
+ def stub_env(key, value)
+ allow(ENV).to receive(:[]).and_call_original unless @env_already_stubbed
+ @env_already_stubbed ||= true
+ allow(ENV).to receive(:[]).with(key).and_return(value)
+ end
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index c79975d8667..90f1a9c8798 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -23,6 +23,7 @@ module TestEnv
'binary-encoding' => '7b1cf43',
'gitattributes' => '5a62481',
'expand-collapse-diffs' => '4842455',
+ 'symlink-expand-diff' => '81e6355',
'expand-collapse-files' => '025db92',
'expand-collapse-lines' => '238e82d',
'video' => '8879059',
@@ -34,6 +35,8 @@ module TestEnv
'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
+ 'deleted-image-test' => '6c17798',
+ 'wip' => 'b9238ee'
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -204,20 +207,18 @@ module TestEnv
end
def set_repo_refs(repo_path, branch_sha)
+ instructions = branch_sha.map {|branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00"
+ update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
+ reset = proc do
+ IO.popen(update_refs, "w") {|io| io.write(instructions) }
+ $?.success?
+ end
+
Dir.chdir(repo_path) do
- branch_sha.each do |branch, sha|
- # Try to reset without fetching to avoid using the network.
- reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha})
- unless system(*reset)
- if system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
- unless system(*reset)
- raise 'The fetched test seed '\
- 'does not contain the required revision.'
- end
- else
- raise 'Could not fetch test seed repository.'
- end
- end
+ # Try to reset without fetching to avoid using the network.
+ unless reset.call
+ raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
+ raise 'The fetched test seed does not contain the required revision.' unless reset.call
end
end
end
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb
new file mode 100644
index 00000000000..02657684b57
--- /dev/null
+++ b/spec/support/time_tracking_shared_examples.rb
@@ -0,0 +1,82 @@
+shared_examples 'issuable time tracker' do
+ it 'renders the sidebar component empty state' do
+ page.within '.time-tracking-no-tracking-pane' do
+ expect(page).to have_content 'No estimate or time spent'
+ end
+ end
+
+ it 'updates the sidebar component when estimate is added' do
+ submit_time('/estimate 3w 1d 1h')
+
+ page.within '.time-tracking-estimate-only-pane' do
+ expect(page).to have_content '3w 1d 1h'
+ end
+ end
+
+ it 'updates the sidebar component when spent is added' do
+ submit_time('/spend 3w 1d 1h')
+
+ page.within '.time-tracking-spend-only-pane' do
+ expect(page).to have_content '3w 1d 1h'
+ end
+ end
+
+ it 'shows the comparison when estimate and spent are added' do
+ submit_time('/estimate 3w 1d 1h')
+ submit_time('/spend 3w 1d 1h')
+
+ page.within '.time-tracking-comparison-pane' do
+ expect(page).to have_content '3w 1d 1h'
+ end
+ end
+
+ it 'updates the sidebar component when estimate is removed' do
+ submit_time('/estimate 3w 1d 1h')
+ submit_time('/remove_estimate')
+
+ page.within '#issuable-time-tracker' do
+ expect(page).to have_content 'No estimate or time spent'
+ end
+ end
+
+ it 'updates the sidebar component when spent is removed' do
+ submit_time('/spend 3w 1d 1h')
+ submit_time('/remove_time_spent')
+
+ page.within '#issuable-time-tracker' do
+ expect(page).to have_content 'No estimate or time spent'
+ end
+ end
+
+ it 'shows the help state when icon is clicked' do
+ page.within '#issuable-time-tracker' do
+ find('.help-button').click
+ expect(page).to have_content 'Track time with slash commands'
+ expect(page).to have_content 'Learn more'
+ end
+ end
+
+ it 'hides the help state when close icon is clicked' do
+ page.within '#issuable-time-tracker' do
+ find('.help-button').click
+ find('.close-help-button').click
+
+ expect(page).not_to have_content 'Track time with slash commands'
+ expect(page).not_to have_content 'Learn more'
+ end
+ end
+
+ it 'displays the correct help url' do
+ page.within '#issuable-time-tracker' do
+ find('.help-button').click
+
+ expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md')
+ end
+ end
+end
+
+def submit_time(slash_command)
+ fill_in 'note[note]', with: slash_command
+ click_button 'Comment'
+ wait_for_ajax
+end
diff --git a/spec/support/upload_helpers.rb b/spec/support/upload_helpers.rb
new file mode 100644
index 00000000000..5eead80c935
--- /dev/null
+++ b/spec/support/upload_helpers.rb
@@ -0,0 +1,16 @@
+require 'fileutils'
+
+module UploadHelpers
+ extend self
+
+ def uploaded_image_temp_path
+ basename = 'banana_sample.gif'
+ orig_path = File.join(Rails.root, 'spec', 'fixtures', basename)
+ tmp_path = File.join(Rails.root, 'tmp', 'tests', basename)
+ # Because we use 'move_to_store' on all uploaders, we create a new
+ # tempfile on each call: the file we return here will be renamed in most
+ # cases.
+ FileUtils.copy(orig_path, tmp_path)
+ tmp_path
+ end
+end