diff --git a/app/assets/javascripts/alert_management/components/alert_sidebar.vue b/app/assets/javascripts/alert_management/components/alert_sidebar.vue index 8957ee410a3..64e4089c85a 100644 --- a/app/assets/javascripts/alert_management/components/alert_sidebar.vue +++ b/app/assets/javascripts/alert_management/components/alert_sidebar.vue @@ -51,9 +51,18 @@ export default {
%{source_title} into"
msgstr ""
-msgid "From Bitbucket"
-msgstr ""
-
-msgid "From Bitbucket Server"
-msgstr ""
-
-msgid "From FogBugz"
-msgstr ""
-
-msgid "From GitLab.com"
-msgstr ""
-
msgid "From Google Code"
msgstr ""
@@ -11053,9 +11041,6 @@ msgstr ""
msgid "Getting started with releases"
msgstr ""
-msgid "Git"
-msgstr ""
-
msgid "Git LFS is not enabled on this GitLab server, contact your admin."
msgstr ""
@@ -14546,6 +14531,9 @@ msgstr ""
msgid "Merge request dependencies"
msgstr ""
+msgid "Merge request was scheduled to merge after pipeline succeeds"
+msgstr ""
+
msgid "Merge requests"
msgstr ""
@@ -16249,9 +16237,6 @@ msgstr ""
msgid "One or more of your %{provider} projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git."
msgstr ""
-msgid "One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git."
-msgstr ""
-
msgid "One or more of your Google Code projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git."
msgstr ""
@@ -17305,9 +17290,6 @@ msgstr ""
msgid "Please convert %{linkStart}them to Git%{linkEnd}, and go through the %{linkToImportFlow} again."
msgstr ""
-msgid "Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again."
-msgstr ""
-
msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again."
msgstr ""
@@ -19234,9 +19216,6 @@ msgstr ""
msgid "Queued"
msgstr ""
-msgid "Quick actions"
-msgstr ""
-
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
@@ -20485,9 +20464,6 @@ msgstr ""
msgid "Search"
msgstr ""
-msgid "Search Button"
-msgstr ""
-
msgid "Search Jira issues"
msgstr ""
@@ -24801,9 +24777,6 @@ msgstr ""
msgid "To start serving your jobs you can either add specific Runners to your project or use shared Runners"
msgstr ""
-msgid "To this GitLab instance"
-msgstr ""
-
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
msgstr ""
@@ -26826,6 +26799,9 @@ msgstr ""
msgid "You"
msgstr ""
+msgid "You already have pending todo for this alert"
+msgstr ""
+
msgid "You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application."
msgstr ""
@@ -27105,6 +27081,9 @@ msgstr ""
msgid "You have imported from this project %{numberOfPreviousImports} times before. Each new import will create duplicate issues."
msgstr ""
+msgid "You have insufficient permissions to create a Todo for this alert"
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -28914,6 +28893,9 @@ msgstr ""
msgid "vulnerability|dismissed"
msgstr ""
+msgid "was scheduled to merge after pipeline succeeds by"
+msgstr ""
+
msgid "wiki page"
msgstr ""
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index cf15cf9256b..c838affa239 100644
--- a/spec/controllers/dashboard_controller_spec.rb
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -55,20 +55,6 @@ RSpec.describe DashboardController do
expect(json_response['count']).to eq(6)
end
-
- describe 'design_activity_events feature flag' do
- context 'it is off' do
- before do
- stub_feature_flags(design_activity_events: false)
- end
-
- it 'excludes design activity' do
- get :activity, params: { format: :json }
-
- expect(json_response['count']).to eq(4)
- end
- end
- end
end
context 'when user has no permission to see the event' do
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 72204cff3ab..469e58c94e7 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -1164,18 +1164,6 @@ RSpec.describe GroupsController do
expect(json_response['count']).to eq(3)
end
-
- context 'the design_activity_events feature flag is disabled' do
- before do
- stub_feature_flags(design_activity_events: false)
- end
-
- it 'does not include the design activity' do
- get_activity
-
- expect(json_response['count']).to eq(1)
- end
- end
end
describe 'GET #issues' do
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index ec38a635c2d..0427715d1ac 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -58,12 +58,12 @@ RSpec.describe Import::BitbucketController do
before do
@repo = double(name: 'vim', slug: 'vim', owner: 'asd', full_name: 'asd/vim', clone_url: 'http://test.host/demo/url.git', 'valid?' => true)
@invalid_repo = double(name: 'mercurialrepo', slug: 'mercurialrepo', owner: 'asd', full_name: 'asd/mercurialrepo', clone_url: 'http://test.host/demo/mercurialrepo.git', 'valid?' => false)
+ allow(controller).to receive(:provider_url).and_return('http://demobitbucket.org')
assign_session_tokens
- stub_feature_flags(new_import_ui: false)
end
- it_behaves_like 'import controller with new_import_ui feature flag' do
+ it_behaves_like 'import controller status' do
before do
allow(controller).to receive(:provider_url).and_return('http://demobitbucket.org')
end
@@ -75,44 +75,16 @@ RSpec.describe Import::BitbucketController do
let(:client_repos_field) { :repos }
end
- context 'with new_import_ui feature flag enabled' do
- before do
- stub_feature_flags(new_import_ui: true)
- allow(controller).to receive(:provider_url).and_return('http://demobitbucket.org')
- end
+ it 'returns invalid repos' do
+ allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo, @invalid_repo])
- it 'returns invalid repos' do
- allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo, @invalid_repo])
+ get :status, format: :json
- get :status, format: :json
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['incompatible_repos'].length).to eq(1)
- expect(json_response.dig("incompatible_repos", 0, "id")).to eq(@invalid_repo.full_name)
- expect(json_response['provider_repos'].length).to eq(1)
- expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name)
- end
- end
-
- it "assigns variables" do
- @project = create(:project, import_type: 'bitbucket', creator_id: user.id)
- allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([@repo])
- expect(assigns(:incompatible_repos)).to eq([])
- end
-
- it "does not show already added project" do
- @project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
- allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([])
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['incompatible_repos'].length).to eq(1)
+ expect(json_response.dig("incompatible_repos", 0, "id")).to eq(@invalid_repo.full_name)
+ expect(json_response['provider_repos'].length).to eq(1)
+ expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name)
end
context 'when filtering' do
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb
index 4aa70d399ce..bb80de6425f 100644
--- a/spec/controllers/import/bitbucket_server_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_server_controller_spec.rb
@@ -148,28 +148,21 @@ RSpec.describe Import::BitbucketServerController do
@invalid_repo = double(slug: 'invalid', project_key: 'foobar', full_name: 'asd/foobar', "valid?" => false, browse_url: 'http://bad-repo', name: 'invalid')
@created_repo = double(slug: 'created', project_key: 'existing', full_name: 'group/created', "valid?" => true, browse_url: 'http://existing')
assign_session_tokens
- stub_feature_flags(new_import_ui: false)
end
- context 'with new_import_ui feature flag enabled' do
- before do
- stub_feature_flags(new_import_ui: true)
- end
+ it 'returns invalid repos' do
+ allow(client).to receive(:repos).with(filter: nil, limit: 25, page_offset: 0).and_return([@repo, @invalid_repo])
- it 'returns invalid repos' do
- allow(client).to receive(:repos).with(filter: nil, limit: 25, page_offset: 0).and_return([@repo, @invalid_repo])
+ get :status, format: :json
- get :status, format: :json
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['incompatible_repos'].length).to eq(1)
- expect(json_response.dig("incompatible_repos", 0, "id")).to eq(@invalid_repo.full_name)
- expect(json_response['provider_repos'].length).to eq(1)
- expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['incompatible_repos'].length).to eq(1)
+ expect(json_response.dig("incompatible_repos", 0, "id")).to eq(@invalid_repo.full_name)
+ expect(json_response['provider_repos'].length).to eq(1)
+ expect(json_response.dig("provider_repos", 0, "id")).to eq(@repo.full_name)
end
- it_behaves_like 'import controller with new_import_ui feature flag' do
+ it_behaves_like 'import controller status' do
let(:repo) { @repo }
let(:repo_id) { @repo.full_name }
let(:import_source) { @repo.browse_url }
@@ -177,47 +170,14 @@ RSpec.describe Import::BitbucketServerController do
let(:client_repos_field) { :repos }
end
- it 'assigns repository categories' do
- created_project = create(:project, :import_finished, import_type: 'bitbucket_server', creator_id: user.id, import_source: @created_repo.browse_url)
-
- expect(repos).to receive(:partition).and_return([[@repo, @created_repo], [@invalid_repo]])
- expect(repos).to receive(:current_page).and_return(1)
- expect(repos).to receive(:next_page).and_return(2)
- expect(repos).to receive(:prev_page).and_return(nil)
- expect(client).to receive(:repos).and_return(repos)
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([created_project])
- expect(assigns(:repos)).to eq([@repo])
- expect(assigns(:incompatible_repos)).to eq([@invalid_repo])
- end
-
context 'when filtering' do
let(:filter) { 'test' }
it 'passes filter param to bitbucket client' do
- expect(repos).to receive(:partition).and_return([[@repo, @created_repo], [@invalid_repo]])
- expect(client).to receive(:repos).with(filter: filter, limit: 25, page_offset: 0).and_return(repos)
+ expect(client).to receive(:repos).with(filter: filter, limit: 25, page_offset: 0).and_return([@repo])
get :status, params: { filter: filter }, as: :json
end
end
end
-
- describe 'GET jobs' do
- before do
- assign_session_tokens
- end
-
- it 'returns a list of imported projects' do
- created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id)
-
- get :jobs
-
- expect(json_response.count).to eq(1)
- expect(json_response.first['id']).to eq(created_project.id)
- expect(json_response.first['import_status']).to eq('none')
- end
- end
end
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
index aabbcb30358..376c089df78 100644
--- a/spec/controllers/import/fogbugz_controller_spec.rb
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -82,36 +82,15 @@ RSpec.describe Import::FogbugzController do
before do
@repo = OpenStruct.new(id: 'demo', name: 'vim')
stub_client(valid?: true)
- stub_feature_flags(new_import_ui: false)
end
- it_behaves_like 'import controller with new_import_ui feature flag' do
+ it_behaves_like 'import controller status' do
let(:repo) { @repo }
let(:repo_id) { @repo.id }
let(:import_source) { @repo.name }
let(:provider_name) { 'fogbugz' }
let(:client_repos_field) { :repos }
end
-
- it 'assigns variables' do
- @project = create(:project, import_type: 'fogbugz', creator_id: user.id)
- stub_client(repos: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([@repo])
- end
-
- it 'does not show already added project' do
- @project = create(:project, import_type: 'fogbugz', creator_id: user.id, import_source: 'vim')
- stub_client(repos: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([])
- end
end
describe 'POST create' do
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index 1cd0593f762..42c4348dac2 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -36,36 +36,15 @@ RSpec.describe Import::GitlabController do
before do
@repo = OpenStruct.new(id: 1, path: 'vim', path_with_namespace: 'asd/vim', web_url: 'https://gitlab.com/asd/vim')
assign_session_token
- stub_feature_flags(new_import_ui: false)
end
- it_behaves_like 'import controller with new_import_ui feature flag' do
+ it_behaves_like 'import controller status' do
let(:repo) { @repo }
let(:repo_id) { @repo.id }
let(:import_source) { @repo.path_with_namespace }
let(:provider_name) { 'gitlab' }
let(:client_repos_field) { :projects }
end
-
- it "assigns variables" do
- @project = create(:project, import_type: 'gitlab', creator_id: user.id)
- stub_client(projects: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([@repo])
- end
-
- it "does not show already added project" do
- @project = create(:project, import_type: 'gitlab', creator_id: user.id, import_source: 'asd/vim')
- stub_client(projects: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([])
- end
end
describe "POST create" do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index b3a83723189..9728fad417e 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -38,9 +38,9 @@ RSpec.describe Projects::NotesController do
end
it 'passes last_fetched_at from headers to NotesFinder and MergeIntoNotesService' do
- last_fetched_at = 3.hours.ago.to_i
+ last_fetched_at = Time.zone.at(3.hours.ago.to_i) # remove nanoseconds
- request.headers['X-Last-Fetched-At'] = last_fetched_at
+ request.headers['X-Last-Fetched-At'] = microseconds(last_fetched_at)
expect(NotesFinder).to receive(:new)
.with(anything, hash_including(last_fetched_at: last_fetched_at))
@@ -84,6 +84,81 @@ RSpec.describe Projects::NotesController do
end
end
+ context 'for multiple pages of notes', :aggregate_failures do
+ # 3 pages worth: 1 normal page, 1 oversized due to clashing updated_at,
+ # and a final, short page
+ let!(:page_1) { create_list(:note, 2, noteable: issue, project: project, updated_at: 3.days.ago) }
+ let!(:page_2) { create_list(:note, 3, noteable: issue, project: project, updated_at: 2.days.ago) }
+ let!(:page_3) { create_list(:note, 2, noteable: issue, project: project, updated_at: 1.day.ago) }
+
+ # Include a resource event in the middle page as well
+ let!(:resource_event) { create(:resource_state_event, issue: issue, user: user, created_at: 2.days.ago) }
+
+ let(:page_1_boundary) { microseconds(page_1.last.updated_at + NotesFinder::FETCH_OVERLAP) }
+ let(:page_2_boundary) { microseconds(page_2.last.updated_at + NotesFinder::FETCH_OVERLAP) }
+
+ around do |example|
+ Timecop.freeze do
+ example.run
+ end
+ end
+
+ before do
+ stub_const('Gitlab::UpdatedNotesPaginator::LIMIT', 2)
+ end
+
+ context 'feature flag enabled' do
+ before do
+ stub_feature_flags(paginated_notes: true)
+ end
+
+ it 'returns the first page of notes' do
+ get :index, params: request_params
+
+ expect(json_response['notes'].count).to eq(page_1.count)
+ expect(json_response['more']).to be_truthy
+ expect(json_response['last_fetched_at']).to eq(page_1_boundary)
+ expect(response.headers['Poll-Interval'].to_i).to eq(1)
+ end
+
+ it 'returns the second page of notes' do
+ request.headers['X-Last-Fetched-At'] = page_1_boundary
+
+ get :index, params: request_params
+
+ expect(json_response['notes'].count).to eq(page_2.count + 1) # resource event
+ expect(json_response['more']).to be_truthy
+ expect(json_response['last_fetched_at']).to eq(page_2_boundary)
+ expect(response.headers['Poll-Interval'].to_i).to eq(1)
+ end
+
+ it 'returns the final page of notes' do
+ request.headers['X-Last-Fetched-At'] = page_2_boundary
+
+ get :index, params: request_params
+
+ expect(json_response['notes'].count).to eq(page_3.count)
+ expect(json_response['more']).to be_falsy
+ expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now))
+ expect(response.headers['Poll-Interval'].to_i).to be > 1
+ end
+ end
+
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(paginated_notes: false)
+ end
+
+ it 'returns all notes' do
+ get :index, params: request_params
+
+ expect(json_response['notes'].count).to eq((page_1 + page_2 + page_3).size + 1)
+ expect(json_response['more']).to be_falsy
+ expect(json_response['last_fetched_at']).to eq(microseconds(Time.zone.now))
+ end
+ end
+ end
+
context 'for a discussion note' do
let(:project) { create(:project, :repository) }
let!(:note) { create(:discussion_note_on_merge_request, project: project) }
@@ -870,4 +945,9 @@ RSpec.describe Projects::NotesController do
end
end
end
+
+ # Convert a time to an integer number of microseconds
+ def microseconds(time)
+ (time.to_i * 1_000_000) + time.usec
+ end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 53d278f9cd0..e59493827ba 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -130,18 +130,6 @@ RSpec.describe ProjectsController do
expect(json_response['count']).to eq(1)
end
-
- context 'the feature flag is disabled' do
- before do
- stub_feature_flags(design_activity_events: false)
- end
-
- it 'returns correct count' do
- get_activity(project)
-
- expect(json_response['count']).to eq(0)
- end
- end
end
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 5610f5889e6..868b126dc28 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -123,7 +123,7 @@ RSpec.describe NotesFinder do
let!(:note1) { create :note_on_commit, project: project }
let!(:note2) { create :note_on_commit, project: project }
let(:commit) { note1.noteable }
- let(:params) { { project: project, target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } }
+ let(:params) { { project: project, target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago } }
it 'finds all notes' do
notes = described_class.new(user, params).execute
@@ -172,7 +172,7 @@ RSpec.describe NotesFinder do
let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) }
- let(:params) { { project: confidential_issue.project, target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } }
+ let(:params) { { project: confidential_issue.project, target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago } }
it 'returns notes if user can see the issue' do
expect(described_class.new(user, params).execute).to eq([confidential_note])
@@ -204,7 +204,7 @@ RSpec.describe NotesFinder do
end
it 'returns the expected notes when last_fetched_at is given' do
- params = { project: project, target: commit, last_fetched_at: 1.hour.ago.to_i }
+ params = { project: project, target: commit, last_fetched_at: 1.hour.ago }
expect(described_class.new(user, params).execute).to eq([note2])
end
diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb
index 21a7d295dde..559d1004b4b 100644
--- a/spec/finders/user_recent_events_finder_spec.rb
+++ b/spec/finders/user_recent_events_finder_spec.rb
@@ -37,28 +37,15 @@ RSpec.describe UserRecentEventsFinder do
expect(finder.execute).to be_empty
end
- describe 'design_activity_events feature flag' do
+ describe 'design activity events' do
let_it_be(:event_a) { create(:design_event, author: project_owner) }
let_it_be(:event_b) { create(:design_event, author: project_owner) }
- context 'the design_activity_events feature-flag is enabled' do
- it 'only includes design events in enabled projects', :aggregate_failures do
- events = finder.execute
+ it 'only includes design events', :aggregate_failures do
+ events = finder.execute
- expect(events).to include(event_a)
- expect(events).to include(event_b)
- end
- end
-
- context 'the design_activity_events feature-flag is disabled' do
- it 'excludes design events', :aggregate_failures do
- stub_feature_flags(design_activity_events: false)
-
- events = finder.execute
-
- expect(events).not_to include(event_a)
- expect(events).not_to include(event_b)
- end
+ expect(events).to include(event_a)
+ expect(events).to include(event_b)
end
end
end
diff --git a/spec/fixtures/clusters/ca_certificate.pem b/spec/fixtures/clusters/ca_certificate.pem
new file mode 100644
index 00000000000..9e6810ab70c
--- /dev/null
+++ b/spec/fixtures/clusters/ca_certificate.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
+RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
+PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
+xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
+Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
+hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
+EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
+FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
+nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
+eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
+hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
+Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
+vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
++OkuE6N36B9K
+-----END CERTIFICATE-----
diff --git a/spec/fixtures/clusters/chain_certificates.pem b/spec/fixtures/clusters/chain_certificates.pem
new file mode 100644
index 00000000000..b8e64d58ee7
--- /dev/null
+++ b/spec/fixtures/clusters/chain_certificates.pem
@@ -0,0 +1,100 @@
+-----BEGIN CERTIFICATE-----
+MIIItjCCB56gAwIBAgIQCu5Ga1hR41iahM0SWhyeNjANBgkqhkiG9w0BAQsFADB1
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk
+IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE5MTIwNDAwMDAwMFoXDTIxMTIwODEy
+MDAwMFowgb0xHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB
+BAGCNzwCAQMTAlVTMRUwEwYLKwYBBAGCNzwCAQITBFV0YWgxFTATBgNVBAUTDDUy
+OTk1MzctMDE0MjELMAkGA1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxDTALBgNVBAcT
+BExlaGkxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMRUwEwYDVQQDEwxkaWdpY2Vy
+dC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAeRYb/RLbljGZ
+IB//DrEdyKYMQqqaJwBlrr3t2paAWNuDJizvVkTMIzdJesI1pA58Myenxp5Dp8GJ
+u/VhBf//v/HAZHUE4xwu104Fg6A1BwUEKgVKERf+7kTt17Lf9fcMIjMyL+FeyPXb
+DOFbH+ej/nYaneFLch2j2xWZg1+Thk0qBlGE8WWAK+fvbEuM0SOeH9RkYFCNGPRS
+KsLn0GvaCnnD4LfNDyMqYop0IpaqXoREEnkRv1MVSOw+hBj497wnnO+/GZegfzwU
+iS60h+PjlDfmdCP18qOS7tRd0qnfU3N3S+PYEd3R63LMcIfbgXNEEWBNKpiH9+8f
+eXq6bXKPAgMBAAGjggT3MIIE8zAfBgNVHSMEGDAWgBQ901Cl1qCt7vNKYApl0yHU
++PjWDzAdBgNVHQ4EFgQUTx0XO7HqD5DOhwlm2p+70uYPBmgwggGjBgNVHREEggGa
+MIIBloIMZGlnaWNlcnQuY29tggl0aGF3dGUuZGWCC2ZyZWVzc2wuY29tggxyYXBp
+ZHNzbC5jb22CDGdlb3RydXN0LmNvbYIJdGhhd3RlLmZyggp0aGF3dGUuY29tghB3
+d3cucmFwaWRzc2wuY29tghB3d3cuZ2VvdHJ1c3QuY29tgg13d3cudGhhd3RlLmZy
+gg13d3cudGhhd3RlLmRlgg53d3cudGhhd3RlLmNvbYIQd3d3LmRpZ2ljZXJ0LmNv
+bYIYa2ItaW50ZXJuYWwuZGlnaWNlcnQuY29tghprbm93bGVkZ2ViYXNlLmRpZ2lj
+ZXJ0LmNvbYIWa25vd2xlZGdlLmRpZ2ljZXJ0LmNvbYIPa2guZGlnaWNlcnQuY29t
+ghlrbm93bGVkZ2VodWIuZGlnaWNlcnQuY29tghh3ZWJzZWN1cml0eS5kaWdpY2Vy
+dC5jb22CFGNvbnRlbnQuZGlnaWNlcnQuY29tgg93d3cuZnJlZXNzbC5jb22CHHd3
+dy53ZWJzZWN1cml0eS5kaWdpY2VydC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1Ud
+JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjB1BgNVHR8EbjBsMDSgMqAwhi5odHRw
+Oi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzIuY3JsMDSgMqAw
+hi5odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzIuY3Js
+MEsGA1UdIAREMEIwNwYJYIZIAYb9bAIBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v
+d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwBwYFZ4EMAQEwgYgGCCsGAQUFBwEBBHwwejAk
+BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFIGCCsGAQUFBzAC
+hkZodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyRXh0ZW5k
+ZWRWYWxpZGF0aW9uU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggF8BgorBgEE
+AdZ5AgQCBIIBbASCAWgBZgB1AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7I
+DdwQAAABbtLkOs4AAAQDAEYwRAIgQ7gh393PInhYfPOhg/lF9yZNRdvjBeufFoG8
+VnBuPNMCIBP8YGC83ig5ttw3ipSRjH0bKj4Ak5O4rynoql9Dy8x3AHYAVhQGmi/X
+wuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0AAAFu0uQ7VgAABAMARzBFAiEAhzE7
+1c48wn3s/30IB4WgxfpLburH0Ku8cchv8QeqcgACIBrWpUlDD18AOfkPCOcB2kWU
+vRXsdptVm3jPeU5TtDSoAHUAu9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e
+0YUAAAFu0uQ60gAABAMARjBEAiBBpH5m7ntGKFTOFgSLcFXRDg66xJqerMy0gOHj
+4TIBYAIgfFABPNy6P61hjiOWwjq73lvoEdAyh18GeFHIp0BgsWEwDQYJKoZIhvcN
+AQELBQADggEBAInaSEqteyQA1zUKiXVqgffhHKZsUq9UnMows6X+UoFPoby9xqm6
+IaY/77zaFZYwXJlP/SvrlbgTLHAdir3y38uhAlfPX4iRuwggOpFFF5hqDckzCm91
+ocGnoG6sUY5mOqKu2vIcZkUQDe+K5gOxI6ME/4YwzWCIcTmBPQ6NQmqiFLPoQty1
+gdbGCcLQNFCuNq4n5OK2NmBjcbtyT4gglat7C4+KV8RkEubZ+MkXzyDkpEXjjzsK
+7iuNB0hRgyyhGzHrlZ/l0OLoT0Cb4I5PzzRSseFEyPKCC1WSF7aE9rFfUqhpqSAT
+7NV7SEijYyFFtuZfz9RGglcqnRlAfgTy+tU=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW
+YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY
+uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/
+LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy
+/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh
+cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k
+8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB
+Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
+BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
+Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy
+dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2
+MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j
+b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW
+gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh
+hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg
+4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa
+2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs
+1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1
+oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn
+8TUoE6smftX3eg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug
+RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm
++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW
+PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM
+xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB
+Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3
+hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg
+EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF
+MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA
+FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec
+nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z
+eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF
+hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2
+Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe
+vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep
++OkuE6N36B9K
+-----END CERTIFICATE-----
diff --git a/spec/fixtures/clusters/intermediate_certificate.pem b/spec/fixtures/clusters/intermediate_certificate.pem
new file mode 100644
index 00000000000..8a81175b746
--- /dev/null
+++ b/spec/fixtures/clusters/intermediate_certificate.pem
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUgkmFf4msdgzANBgkqhkiG9w0BAQsFADBs
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j
+ZSBFViBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowdTEL
+MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3
+LmRpZ2ljZXJ0LmNvbTE0MDIGA1UEAxMrRGlnaUNlcnQgU0hBMiBFeHRlbmRlZCBW
+YWxpZGF0aW9uIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBANdTpARR+JmmFkhLZyeqk0nQOe0MsLAAh/FnKIaFjI5j2ryxQDji0/XspQUY
+uD0+xZkXMuwYjPrxDKZkIYXLBxA0sFKIKx9om9KxjxKws9LniB8f7zh3VFNfgHk/
+LhqqqB5LKw2rt2O5Nbd9FLxZS99RStKh4gzikIKHaq7q12TWmFXo/a8aUGxUvBHy
+/Urynbt/DvTVvo4WiRJV2MBxNO723C3sxIclho3YIeSwTQyJ3DkmF93215SF2AQh
+cJ1vb/9cuhnhRctWVyh+HA1BV6q3uCe7seT6Ku8hI3UarS2bhjWMnHe1c63YlC3k
+8wyd7sFOYn4XwHGeLN7x+RAoGTMCAwEAAaOCAUkwggFFMBIGA1UdEwEB/wQIMAYB
+Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
+BQcDAjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp
+Z2ljZXJ0LmNvbTBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsNC5kaWdpY2Vy
+dC5jb20vRGlnaUNlcnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3JsMD0GA1UdIAQ2
+MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5j
+b20vQ1BTMB0GA1UdDgQWBBQ901Cl1qCt7vNKYApl0yHU+PjWDzAfBgNVHSMEGDAW
+gBSxPsNpA/i/RwHUmCYaCALvY2QrwzANBgkqhkiG9w0BAQsFAAOCAQEAnbbQkIbh
+hgLtxaDwNBx0wY12zIYKqPBKikLWP8ipTa18CK3mtlC4ohpNiAexKSHc59rGPCHg
+4xFJcKx6HQGkyhE6V6t9VypAdP3THYUYUN9XR3WhfVUgLkc3UHKMf4Ib0mKPLQNa
+2sPIoc4sUqIAY+tzunHISScjl2SFnjgOrWNoPLpSgVh5oywM395t6zHyuqB8bPEs
+1OG9d4Q3A84ytciagRpKkk47RpqF/oOi+Z6Mo8wNXrM9zwR4jxQUezKcxwCmXMS1
+oVWNWlZopCJwqjyBcdmdqEU79OX2olHdx3ti6G8MdOu42vi/hw15UJGQmxg7kVkn
+8TUoE6smftX3eg==
+-----END CERTIFICATE-----
diff --git a/spec/fixtures/clusters/root_certificate.pem b/spec/fixtures/clusters/root_certificate.pem
new file mode 100644
index 00000000000..40107bd837d
--- /dev/null
+++ b/spec/fixtures/clusters/root_certificate.pem
@@ -0,0 +1,49 @@
+-----BEGIN CERTIFICATE-----
+MIIItjCCB56gAwIBAgIQCu5Ga1hR41iahM0SWhyeNjANBgkqhkiG9w0BAQsFADB1
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk
+IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE5MTIwNDAwMDAwMFoXDTIxMTIwODEy
+MDAwMFowgb0xHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB
+BAGCNzwCAQMTAlVTMRUwEwYLKwYBBAGCNzwCAQITBFV0YWgxFTATBgNVBAUTDDUy
+OTk1MzctMDE0MjELMAkGA1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxDTALBgNVBAcT
+BExlaGkxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMRUwEwYDVQQDEwxkaWdpY2Vy
+dC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAeRYb/RLbljGZ
+IB//DrEdyKYMQqqaJwBlrr3t2paAWNuDJizvVkTMIzdJesI1pA58Myenxp5Dp8GJ
+u/VhBf//v/HAZHUE4xwu104Fg6A1BwUEKgVKERf+7kTt17Lf9fcMIjMyL+FeyPXb
+DOFbH+ej/nYaneFLch2j2xWZg1+Thk0qBlGE8WWAK+fvbEuM0SOeH9RkYFCNGPRS
+KsLn0GvaCnnD4LfNDyMqYop0IpaqXoREEnkRv1MVSOw+hBj497wnnO+/GZegfzwU
+iS60h+PjlDfmdCP18qOS7tRd0qnfU3N3S+PYEd3R63LMcIfbgXNEEWBNKpiH9+8f
+eXq6bXKPAgMBAAGjggT3MIIE8zAfBgNVHSMEGDAWgBQ901Cl1qCt7vNKYApl0yHU
++PjWDzAdBgNVHQ4EFgQUTx0XO7HqD5DOhwlm2p+70uYPBmgwggGjBgNVHREEggGa
+MIIBloIMZGlnaWNlcnQuY29tggl0aGF3dGUuZGWCC2ZyZWVzc2wuY29tggxyYXBp
+ZHNzbC5jb22CDGdlb3RydXN0LmNvbYIJdGhhd3RlLmZyggp0aGF3dGUuY29tghB3
+d3cucmFwaWRzc2wuY29tghB3d3cuZ2VvdHJ1c3QuY29tgg13d3cudGhhd3RlLmZy
+gg13d3cudGhhd3RlLmRlgg53d3cudGhhd3RlLmNvbYIQd3d3LmRpZ2ljZXJ0LmNv
+bYIYa2ItaW50ZXJuYWwuZGlnaWNlcnQuY29tghprbm93bGVkZ2ViYXNlLmRpZ2lj
+ZXJ0LmNvbYIWa25vd2xlZGdlLmRpZ2ljZXJ0LmNvbYIPa2guZGlnaWNlcnQuY29t
+ghlrbm93bGVkZ2VodWIuZGlnaWNlcnQuY29tghh3ZWJzZWN1cml0eS5kaWdpY2Vy
+dC5jb22CFGNvbnRlbnQuZGlnaWNlcnQuY29tgg93d3cuZnJlZXNzbC5jb22CHHd3
+dy53ZWJzZWN1cml0eS5kaWdpY2VydC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1Ud
+JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjB1BgNVHR8EbjBsMDSgMqAwhi5odHRw
+Oi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzIuY3JsMDSgMqAw
+hi5odHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1ldi1zZXJ2ZXItZzIuY3Js
+MEsGA1UdIAREMEIwNwYJYIZIAYb9bAIBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v
+d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwBwYFZ4EMAQEwgYgGCCsGAQUFBwEBBHwwejAk
+BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFIGCCsGAQUFBzAC
+hkZodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyRXh0ZW5k
+ZWRWYWxpZGF0aW9uU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggF8BgorBgEE
+AdZ5AgQCBIIBbASCAWgBZgB1AKS5CZC0GFgUh7sTosxncAo8NZgE+RvfuON3zQ7I
+DdwQAAABbtLkOs4AAAQDAEYwRAIgQ7gh393PInhYfPOhg/lF9yZNRdvjBeufFoG8
+VnBuPNMCIBP8YGC83ig5ttw3ipSRjH0bKj4Ak5O4rynoql9Dy8x3AHYAVhQGmi/X
+wuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0AAAFu0uQ7VgAABAMARzBFAiEAhzE7
+1c48wn3s/30IB4WgxfpLburH0Ku8cchv8QeqcgACIBrWpUlDD18AOfkPCOcB2kWU
+vRXsdptVm3jPeU5TtDSoAHUAu9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e
+0YUAAAFu0uQ60gAABAMARjBEAiBBpH5m7ntGKFTOFgSLcFXRDg66xJqerMy0gOHj
+4TIBYAIgfFABPNy6P61hjiOWwjq73lvoEdAyh18GeFHIp0BgsWEwDQYJKoZIhvcN
+AQELBQADggEBAInaSEqteyQA1zUKiXVqgffhHKZsUq9UnMows6X+UoFPoby9xqm6
+IaY/77zaFZYwXJlP/SvrlbgTLHAdir3y38uhAlfPX4iRuwggOpFFF5hqDckzCm91
+ocGnoG6sUY5mOqKu2vIcZkUQDe+K5gOxI6ME/4YwzWCIcTmBPQ6NQmqiFLPoQty1
+gdbGCcLQNFCuNq4n5OK2NmBjcbtyT4gglat7C4+KV8RkEubZ+MkXzyDkpEXjjzsK
+7iuNB0hRgyyhGzHrlZ/l0OLoT0Cb4I5PzzRSseFEyPKCC1WSF7aE9rFfUqhpqSAT
+7NV7SEijYyFFtuZfz9RGglcqnRlAfgTy+tU=
+-----END CERTIFICATE-----
diff --git a/spec/fixtures/product_analytics/event.json b/spec/fixtures/product_analytics/event.json
new file mode 100644
index 00000000000..3100b068a0c
--- /dev/null
+++ b/spec/fixtures/product_analytics/event.json
@@ -0,0 +1,16 @@
+{
+ "aid":"1",
+ "p":"web",
+ "tna":"sp",
+ "tv":"js-2.14.0",
+ "eid":"fbf14096-74ee-47e4-883c-8a0d6cb72e37",
+ "duid":"79543c31-cfc3-4479-a737-fafb9333c8ba",
+ "sid":"54f6d3f3-f4f9-4fdc-87e0-a2c775234c1b",
+ "vid":4,
+ "url":"http://example.com/products/1",
+ "refr":"http://example.com/products/1",
+ "lang":"en-US",
+ "cookie":"1",
+ "tz":"America/Los_Angeles",
+ "cs":"UTF-8"
+}
diff --git a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
new file mode 100644
index 00000000000..fe08cf2c10a
--- /dev/null
+++ b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
@@ -0,0 +1,76 @@
+import { mount } from '@vue/test-utils';
+import SidebarTodo from '~/alert_management/components/sidebar/sidebar_todo.vue';
+import AlertMarkTodo from '~/alert_management/graphql/mutations/alert_todo_create.graphql';
+import mockAlerts from '../mocks/alerts.json';
+
+const mockAlert = mockAlerts[0];
+
+describe('Alert Details Sidebar To Do', () => {
+ let wrapper;
+
+ function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) {
+ wrapper = mount(SidebarTodo, {
+ propsData: {
+ alert: { ...mockAlert },
+ ...data,
+ sidebarCollapsed,
+ projectPath: 'projectPath',
+ },
+ mocks: {
+ $apollo: {
+ mutate: jest.fn(),
+ queries: {
+ alert: {
+ loading,
+ },
+ },
+ },
+ },
+ stubs,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('updating the alert to do', () => {
+ const mockUpdatedMutationResult = {
+ data: {
+ updateAlertTodo: {
+ errors: [],
+ alert: {},
+ },
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({
+ data: { alert: mockAlert },
+ sidebarCollapsed: false,
+ loading: false,
+ });
+ });
+
+ it('renders a button for adding a To Do', () => {
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find('[data-testid="alert-todo-button"]').text()).toBe('Add a To Do');
+ });
+ });
+
+ it('calls `$apollo.mutate` with `AlertMarkTodo` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+
+ return wrapper.vm.$nextTick().then(() => {
+ wrapper.find('button').trigger('click');
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: AlertMarkTodo,
+ variables: {
+ iid: '1527542',
+ projectPath: 'projectPath',
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
new file mode 100644
index 00000000000..11ee40a4c7e
--- /dev/null
+++ b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::AlertManagement::Alerts::Todo::Create do
+ subject(:mutation) { described_class.new(object: project, context: { current_user: current_user }, field: nil) }
+
+ let_it_be(:alert) { create(:alert_management_alert) }
+ let_it_be(:project) { alert.project }
+ let(:current_user) { project.owner }
+
+ let(:args) { { project_path: project.full_path, iid: alert.iid } }
+
+ specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) }
+
+ describe '#resolve' do
+ subject(:resolve) { mutation.resolve(args) }
+
+ context 'when user does not have permissions' do
+ let(:current_user) { nil }
+
+ specify { expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) }
+ end
+
+ context 'when project is invalid' do
+ let(:args) { { project_path: 'bunk/path', iid: alert.iid } }
+
+ specify { expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) }
+ end
+
+ context 'when alert is invalid' do
+ let(:args) { { project_path: project.full_path, iid: "-1" } }
+
+ specify { expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) }
+ end
+
+ context 'when the create service yields errors' do
+ let(:error_response) { double(error?: true, message: 'error', payload: { alert: {} }) }
+
+ before do
+ allow_next_instance_of(::AlertManagement::Alerts::Todo::CreateService) do |service|
+ allow(service).to receive(:execute).and_return(error_response)
+ end
+ end
+
+ specify { expect { resolve }.not_to change(Todo, :count) }
+ specify { expect(resolve[:errors]).to eq([error_response.message]) }
+ end
+
+ context 'with valid inputs' do
+ it 'creates a new todo' do
+ expect { resolve }.to change { Todo.where(user: current_user, action: Todo::MARKED).count }.by(1)
+ end
+
+ it { is_expected.to eq(alert: alert, todo: Todo.last, errors: []) }
+ end
+ end
+end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index f0de602ed2d..4ca31405c1e 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -255,14 +255,6 @@ RSpec.describe EventsHelper do
it { is_expected.to be(true) }
end
- context 'the feature flag is off' do
- before do
- stub_feature_flags(design_activity_events: false)
- end
-
- it { is_expected.to be(false) }
- end
-
context 'a project has been assigned' do
before do
assign(:project, project)
@@ -277,14 +269,6 @@ RSpec.describe EventsHelper do
it { is_expected.to be(false) }
end
-
- context 'the feature flag is off' do
- before do
- stub_feature_flags(design_activity_events: false)
- end
-
- it { is_expected.to be(false) }
- end
end
context 'projects have been assigned' do
@@ -309,14 +293,6 @@ RSpec.describe EventsHelper do
it { is_expected.to be(false) }
end
-
- context 'the feature flag is off' do
- before do
- stub_feature_flags(design_activity_events: false)
- end
-
- it { is_expected.to be(false) }
- end
end
context 'a group has been assigned' do
@@ -344,14 +320,6 @@ RSpec.describe EventsHelper do
it { is_expected.to be(false) }
end
-
- context 'the feature flag is off' do
- before do
- stub_feature_flags(design_activity_events: false)
- end
-
- it { is_expected.to be(false) }
- end
end
end
end
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index 0125f171ecb..bab48796b8c 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -88,16 +88,6 @@ RSpec.describe EventFilter do
it 'returns only design events' do
expect(filtered_events).to contain_exactly(design_event)
end
-
- context 'the :design_activity_events feature is disabled' do
- before do
- stub_feature_flags(design_activity_events: false)
- end
-
- it 'does not return design events' do
- expect(filtered_events).to match_array(Event.not_design)
- end
- end
end
context 'with the "wiki" filter' do
diff --git a/spec/lib/gitlab/updated_notes_paginator_spec.rb b/spec/lib/gitlab/updated_notes_paginator_spec.rb
new file mode 100644
index 00000000000..eedc11777d4
--- /dev/null
+++ b/spec/lib/gitlab/updated_notes_paginator_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::UpdatedNotesPaginator do
+ let(:issue) { create(:issue) }
+
+ let(:project) { issue.project }
+ let(:finder) { NotesFinder.new(user, target: issue, last_fetched_at: last_fetched_at) }
+ let(:user) { issue.author }
+
+ let!(:page_1) { create_list(:note, 2, noteable: issue, project: project, updated_at: 2.days.ago) }
+ let!(:page_2) { [create(:note, noteable: issue, project: project, updated_at: 1.day.ago)] }
+
+ let(:page_1_boundary) { page_1.last.updated_at + NotesFinder::FETCH_OVERLAP }
+
+ around do |example|
+ Timecop.freeze do
+ example.run
+ end
+ end
+
+ before do
+ stub_const("Gitlab::UpdatedNotesPaginator::LIMIT", 2)
+ end
+
+ subject(:paginator) { described_class.new(finder.execute, last_fetched_at: last_fetched_at) }
+
+ describe 'last_fetched_at: start of time' do
+ let(:last_fetched_at) { Time.at(0) }
+
+ it 'calculates the first page of notes', :aggregate_failures do
+ expect(paginator.notes).to match_array(page_1)
+ expect(paginator.metadata).to match(
+ more: true,
+ last_fetched_at: microseconds(page_1_boundary)
+ )
+ end
+ end
+
+ describe 'last_fetched_at: start of final page' do
+ let(:last_fetched_at) { page_1_boundary }
+
+ it 'calculates a final page', :aggregate_failures do
+ expect(paginator.notes).to match_array(page_2)
+ expect(paginator.metadata).to match(
+ more: false,
+ last_fetched_at: microseconds(Time.zone.now)
+ )
+ end
+ end
+
+ # Convert a time to an integer number of microseconds
+ def microseconds(time)
+ (time.to_i * 1_000_000) + time.usec
+ end
+end
diff --git a/spec/lib/product_analytics/event_params_spec.rb b/spec/lib/product_analytics/event_params_spec.rb
new file mode 100644
index 00000000000..d6c098599d6
--- /dev/null
+++ b/spec/lib/product_analytics/event_params_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProductAnalytics::EventParams do
+ describe '.parse_event_params' do
+ subject { described_class.parse_event_params(raw_event) }
+
+ let(:raw_event) { Gitlab::Json.parse(fixture_file('product_analytics/event.json')) }
+
+ it 'extracts all params from raw event' do
+ expected_params = {
+ project_id: '1',
+ platform: 'web',
+ name_tracker: 'sp',
+ v_tracker: 'js-2.14.0',
+ event_id: 'fbf14096-74ee-47e4-883c-8a0d6cb72e37',
+ domain_userid: '79543c31-cfc3-4479-a737-fafb9333c8ba',
+ domain_sessionid: '54f6d3f3-f4f9-4fdc-87e0-a2c775234c1b',
+ domain_sessionidx: 4,
+ page_url: 'http://example.com/products/1',
+ page_referrer: 'http://example.com/products/1',
+ br_lang: 'en-US',
+ br_cookies: true,
+ os_timezone: 'America/Los_Angeles',
+ doc_charset: 'UTF-8'
+ }
+
+ expect(subject).to include(expected_params)
+ end
+ end
+
+ describe '.has_required_params?' do
+ subject { described_class.has_required_params?(params) }
+
+ context 'aid and eid are present' do
+ let(:params) { { 'aid' => 1, 'eid' => 2 } }
+
+ it { expect(subject).to be_truthy }
+ end
+
+ context 'aid and eid are missing' do
+ let(:params) { {} }
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'eid is missing' do
+ let(:params) { { 'aid' => 1 } }
+
+ it { expect(subject).to be_falsey }
+ end
+ end
+end
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
index fb523092f7a..477fb16400a 100644
--- a/spec/mailers/emails/merge_requests_spec.rb
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -17,4 +17,20 @@ RSpec.describe Emails::MergeRequests do
expect(subject).to have_body_text current_user.name
end
end
+
+ describe "#merge_when_pipeline_succeeds_email" do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:current_user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:title) { "Merge request #{merge_request.to_reference} was scheduled to merge after pipeline succeeds by #{current_user.name}" }
+
+ subject { Notify.merge_when_pipeline_succeeds_email(user.id, merge_request.id, current_user.id) }
+
+ it "has required details" do
+ expect(subject).to have_content title
+ expect(subject).to have_content merge_request.to_reference
+ expect(subject).to have_content current_user.name
+ end
+ end
end
diff --git a/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb b/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb
new file mode 100644
index 00000000000..81fa29f4c54
--- /dev/null
+++ b/spec/migrations/remove_gitlab_issue_tracker_service_records_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200623142159_remove_gitlab_issue_tracker_service_records.rb')
+
+RSpec.describe RemoveGitlabIssueTrackerServiceRecords do
+ let(:services) { table(:services) }
+
+ before do
+ 5.times { services.create!(type: 'GitlabIssueTrackerService') }
+ services.create!(type: 'SomeOtherType')
+ end
+
+ it 'removes services records of type GitlabIssueTrackerService', :aggregate_failures do
+ expect { migrate! }.to change { services.count }.from(6).to(1)
+ expect(services.first.type).to eq('SomeOtherType')
+ expect(services.where(type: 'GitlabIssueTrackerService')).to be_empty
+ end
+end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index 06ecd674928..cc314d9077d 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -58,4 +58,11 @@ RSpec.describe ApplicationRecord do
expect(MergeRequest.underscore).to eq('merge_request')
end
end
+
+ describe '.at_most' do
+ it 'limits the number of records returned' do
+ create_list(:user, 3)
+ expect(User.at_most(2).count).to eq(2)
+ end
+ end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index de5cd01df55..adccc72d13d 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -204,6 +204,52 @@ RSpec.describe Clusters::Platforms::Kubernetes do
end
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::KubeClient) }
+
+ context 'ca_pem is a single certificate' do
+ let(:ca_pem) { File.read(Rails.root.join('spec/fixtures/clusters/ca_certificate.pem')) }
+ let(:kubernetes) do
+ build(:cluster_platform_kubernetes,
+ :configured,
+ namespace: 'a-namespace',
+ cluster: cluster,
+ ca_pem: ca_pem)
+ end
+
+ it 'adds it to cert_store' do
+ cert = OpenSSL::X509::Certificate.new(ca_pem)
+ cert_store = kubernetes.kubeclient.kubeclient_options[:ssl_options][:cert_store]
+
+ expect(cert_store.verify(cert)).to be true
+ end
+ end
+
+ context 'ca_pem is a chain' do
+ let(:cert_chain) { File.read(Rails.root.join('spec/fixtures/clusters/chain_certificates.pem')) }
+ let(:kubernetes) do
+ build(:cluster_platform_kubernetes,
+ :configured,
+ namespace: 'a-namespace',
+ cluster: cluster,
+ ca_pem: cert_chain)
+ end
+
+ it 'includes chain of certificates' do
+ cert1_file = File.read(Rails.root.join('spec/fixtures/clusters/root_certificate.pem'))
+ cert1 = OpenSSL::X509::Certificate.new(cert1_file)
+
+ cert2_file = File.read(Rails.root.join('spec/fixtures/clusters/intermediate_certificate.pem'))
+ cert2 = OpenSSL::X509::Certificate.new(cert2_file)
+
+ cert3_file = File.read(Rails.root.join('spec/fixtures/clusters/ca_certificate.pem'))
+ cert3 = OpenSSL::X509::Certificate.new(cert3_file)
+
+ cert_store = kubernetes.kubeclient.kubeclient_options[:ssl_options][:cert_store]
+
+ expect(cert_store.verify(cert1)).to be true
+ expect(cert_store.verify(cert2)).to be true
+ expect(cert_store.verify(cert3)).to be true
+ end
+ end
end
describe '#rbac?' do
diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb
index a1773378073..aca2a8c3a2f 100644
--- a/spec/models/event_collection_spec.rb
+++ b/spec/models/event_collection_spec.rb
@@ -50,24 +50,6 @@ RSpec.describe EventCollection do
expect(events).to include(wiki_page_event)
end
- context 'the design_activity_events feature flag is disabled' do
- before do
- stub_feature_flags(design_activity_events: false)
- end
-
- it 'omits the design events when using to_a' do
- events = described_class.new(projects).to_a
-
- expect(events).not_to include(design_event)
- end
-
- it 'omits the wiki page events when using all_project_events' do
- events = described_class.new(projects).all_project_events
-
- expect(events).not_to include(design_event)
- end
- end
-
it 'includes the design events' do
collection = described_class.new(projects)
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index a05ae188ef6..96baeab6809 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -643,15 +643,6 @@ RSpec.describe Event do
end
end
- describe '.not_design' do
- it 'does not contain the design events' do
- non_design_events = events.reject(&:design?)
-
- expect(events).not_to match_array(non_design_events)
- expect(described_class.not_design).to match_array(non_design_events)
- end
- end
-
describe '.for_wiki_page' do
it 'only contains the wiki page events' do
wiki_events = events.select(&:wiki_page?)
diff --git a/spec/models/product_analytics_event_spec.rb b/spec/models/product_analytics_event_spec.rb
index 6593edae8ac..6058df9fa13 100644
--- a/spec/models/product_analytics_event_spec.rb
+++ b/spec/models/product_analytics_event_spec.rb
@@ -5,6 +5,13 @@ RSpec.describe ProductAnalyticsEvent, type: :model do
it { is_expected.to belong_to(:project) }
it { expect(described_class).to respond_to(:order_by_time) }
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project_id) }
+ it { is_expected.to validate_presence_of(:event_id) }
+ it { is_expected.to validate_presence_of(:v_collector) }
+ it { is_expected.to validate_presence_of(:v_etl) }
+ end
+
describe '.timerange' do
let_it_be(:event_1) { create(:product_analytics_event, collector_tstamp: Time.zone.now - 1.day) }
let_it_be(:event_2) { create(:product_analytics_event, collector_tstamp: Time.zone.now - 5.days) }
diff --git a/spec/requests/api/admin/instance_clusters_spec.rb b/spec/requests/api/admin/instance_clusters_spec.rb
new file mode 100644
index 00000000000..b68541b5d92
--- /dev/null
+++ b/spec/requests/api/admin/instance_clusters_spec.rb
@@ -0,0 +1,461 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::API::Admin::InstanceClusters do
+ include KubernetesHelpers
+
+ let_it_be(:regular_user) { create(:user) }
+ let_it_be(:admin_user) { create(:admin) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_cluster) do
+ create(:cluster, :project, :provided_by_gcp,
+ user: admin_user,
+ projects: [project])
+ end
+ let(:project_cluster_id) { project_cluster.id }
+
+ describe "GET /admin/clusters" do
+ let_it_be(:clusters) do
+ create_list(:cluster, 3, :provided_by_gcp, :instance, :production_environment)
+ end
+
+ context "when authenticated as a non-admin user" do
+ it 'returns 403' do
+ get api('/admin/clusters', regular_user)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context "when authenticated as admin" do
+ before do
+ get api("/admin/clusters", admin_user)
+ end
+
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'includes pagination headers' do
+ expect(response).to include_pagination_headers
+ end
+
+ it 'only returns the instance clusters' do
+ cluster_ids = json_response.map { |cluster| cluster['id'] }
+ expect(cluster_ids).to match_array(clusters.pluck(:id))
+ expect(cluster_ids).not_to include(project_cluster_id)
+ end
+ end
+ end
+
+ describe "GET /admin/clusters/:cluster_id" do
+ let_it_be(:platform_kubernetes) do
+ create(:cluster_platform_kubernetes, :configured)
+ end
+
+ let_it_be(:cluster) do
+ create(:cluster, :instance, :provided_by_gcp, :with_domain,
+ platform_kubernetes: platform_kubernetes,
+ user: admin_user)
+ end
+
+ let(:cluster_id) { cluster.id }
+
+ context "when authenticated as admin" do
+ before do
+ get api("/admin/clusters/#{cluster_id}", admin_user)
+ end
+
+ context "when no cluster associated to the ID" do
+ let(:cluster_id) { 1337 }
+
+ it 'returns 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context "when cluster with cluster_id exists" do
+ it 'returns 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns the cluster with cluster_id' do
+ expect(json_response['id']).to eq(cluster.id)
+ end
+
+ it 'returns the cluster information' do
+ expect(json_response['provider_type']).to eq('gcp')
+ expect(json_response['platform_type']).to eq('kubernetes')
+ expect(json_response['environment_scope']).to eq('*')
+ expect(json_response['cluster_type']).to eq('instance_type')
+ expect(json_response['domain']).to eq('example.com')
+ end
+
+ it 'returns kubernetes platform information' do
+ platform = json_response['platform_kubernetes']
+
+ expect(platform['api_url']).to eq('https://kubernetes.example.com')
+ expect(platform['ca_cert']).to be_present
+ end
+
+ it 'returns user information' do
+ user = json_response['user']
+
+ expect(user['id']).to eq(admin_user.id)
+ expect(user['username']).to eq(admin_user.username)
+ end
+
+ it 'returns GCP provider information' do
+ gcp_provider = json_response['provider_gcp']
+
+ expect(gcp_provider['cluster_id']).to eq(cluster.id)
+ expect(gcp_provider['status_name']).to eq('created')
+ expect(gcp_provider['gcp_project_id']).to eq('test-gcp-project')
+ expect(gcp_provider['zone']).to eq('us-central1-a')
+ expect(gcp_provider['machine_type']).to eq('n1-standard-2')
+ expect(gcp_provider['num_nodes']).to eq(3)
+ expect(gcp_provider['endpoint']).to eq('111.111.111.111')
+ end
+
+ context 'when cluster has no provider' do
+ let(:cluster) do
+ create(:cluster, :instance, :provided_by_user, :production_environment)
+ end
+
+ it 'does not include GCP provider info' do
+ expect(json_response['provider_gcp']).not_to be_present
+ end
+ end
+
+ context 'when trying to get a project cluster via the instance cluster endpoint' do
+ it 'returns 404' do
+ get api("/admin/clusters/#{project_cluster_id}", admin_user)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context "when authenticated as a non-admin user" do
+ it 'returns 403' do
+ get api("/admin/clusters/#{cluster_id}", regular_user)
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ describe "POST /admin/clusters/add" do
+ let(:api_url) { 'https://example.com' }
+ let(:authorization_type) { 'rbac' }
+ let(:clusterable) { Clusters::Instance.new }
+
+ let(:platform_kubernetes_attributes) do
+ {
+ api_url: api_url,
+ token: 'sample-token',
+ authorization_type: authorization_type
+ }
+ end
+
+ let(:cluster_params) do
+ {
+ name: 'test-instance-cluster',
+ domain: 'domain.example.com',
+ managed: false,
+ platform_kubernetes_attributes: platform_kubernetes_attributes,
+ clusterable: clusterable
+ }
+ end
+
+ let(:multiple_cluster_params) do
+ {
+ name: 'multiple-instance-cluster',
+ environment_scope: 'staging/*',
+ platform_kubernetes_attributes: platform_kubernetes_attributes
+ }
+ end
+
+ let(:invalid_cluster_params) do
+ {
+ environment_scope: 'production/*',
+ domain: 'domain.example.com',
+ platform_kubernetes_attributes: platform_kubernetes_attributes
+ }
+ end
+
+ context 'authorized user' do
+ before do
+ post api('/admin/clusters/add', admin_user), params: cluster_params
+ end
+
+ context 'with valid params' do
+ it 'responds with 201' do
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
+ it 'creates a new Clusters::Cluster', :aggregate_failures do
+ cluster_result = Clusters::Cluster.find(json_response["id"])
+ platform_kubernetes = cluster_result.platform
+ expect(cluster_result).to be_user
+ expect(cluster_result).to be_kubernetes
+ expect(cluster_result.clusterable).to be_a Clusters::Instance
+ expect(cluster_result.cluster_type).to eq('instance_type')
+ expect(cluster_result.name).to eq('test-instance-cluster')
+ expect(cluster_result.domain).to eq('domain.example.com')
+ expect(cluster_result.environment_scope).to eq('*')
+ expect(cluster_result.enabled).to eq(true)
+ expect(platform_kubernetes.authorization_type).to eq('rbac')
+ expect(cluster_result.managed).to be_falsy
+ expect(platform_kubernetes.api_url).to eq("https://example.com")
+ expect(platform_kubernetes.token).to eq('sample-token')
+ end
+
+ context 'when user does not indicate authorization type' do
+ let(:platform_kubernetes_attributes) do
+ {
+ api_url: api_url,
+ token: 'sample-token'
+ }
+ end
+
+ it 'defaults to RBAC' do
+ cluster_result = Clusters::Cluster.find(json_response['id'])
+
+ expect(cluster_result.platform_kubernetes.rbac?).to be_truthy
+ end
+ end
+
+ context 'when user sets authorization type as ABAC' do
+ let(:authorization_type) { 'abac' }
+
+ it 'creates an ABAC cluster' do
+ cluster_result = Clusters::Cluster.find(json_response['id'])
+
+ expect(cluster_result.platform.abac?).to be_truthy
+ end
+ end
+
+ context 'when an instance cluster already exists' do
+ it 'allows user to add multiple clusters' do
+ post api('/admin/clusters/add', admin_user), params: multiple_cluster_params
+
+ expect(Clusters::Instance.new.clusters.count).to eq(2)
+ end
+ end
+ end
+
+ context 'with invalid params' do
+ context 'when missing a required parameter' do
+ it 'responds with 400' do
+ post api('/admin/clusters/add', admin_user), params: invalid_cluster_params
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eql('name is missing')
+ end
+ end
+
+ context 'with a malformed api url' do
+ let(:api_url) { 'invalid_api_url' }
+
+ it 'responds with 400' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns validation errors' do
+ expect(json_response['message']['platform_kubernetes.api_url'].first).to be_present
+ end
+ end
+ end
+ end
+
+ context 'non-authorized user' do
+ it 'responds with 403' do
+ post api('/admin/clusters/add', regular_user), params: cluster_params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ describe 'PUT /admin/clusters/:cluster_id' do
+ let(:api_url) { 'https://example.com' }
+
+ let(:update_params) do
+ {
+ domain: domain,
+ platform_kubernetes_attributes: platform_kubernetes_attributes
+ }
+ end
+
+ let(:domain) { 'new-domain.com' }
+ let(:platform_kubernetes_attributes) { {} }
+
+ let_it_be(:cluster) do
+ create(:cluster, :instance, :provided_by_gcp, domain: 'old-domain.com')
+ end
+
+ context 'authorized user' do
+ before do
+ put api("/admin/clusters/#{cluster.id}", admin_user), params: update_params
+
+ cluster.reload
+ end
+
+ context 'with valid params' do
+ it 'responds with 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'updates cluster attributes' do
+ expect(cluster.domain).to eq('new-domain.com')
+ end
+ end
+
+ context 'with invalid params' do
+ let(:domain) { 'invalid domain' }
+
+ it 'responds with 400' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'does not update cluster attributes' do
+ expect(cluster.domain).to eq('old-domain.com')
+ end
+
+ it 'returns validation errors' do
+ expect(json_response['message']['domain'].first).to match('contains invalid characters (valid characters: [a-z0-9\\-])')
+ end
+ end
+
+ context 'with a GCP cluster' do
+ context 'when user tries to change GCP specific fields' do
+ let(:platform_kubernetes_attributes) do
+ {
+ api_url: 'https://new-api-url.com',
+ token: 'new-sample-token'
+ }
+ end
+
+ it 'responds with 400' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns validation error' do
+ expect(json_response['message']['platform_kubernetes.base'].first).to eq(_('Cannot modify managed Kubernetes cluster'))
+ end
+ end
+
+ context 'when user tries to change domain' do
+ let(:domain) { 'new-domain.com' }
+
+ it 'responds with 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'with an user cluster' do
+ let(:api_url) { 'https://new-api-url.com' }
+
+ let(:cluster) do
+ create(:cluster, :instance, :provided_by_user, :production_environment)
+ end
+
+ let(:platform_kubernetes_attributes) do
+ {
+ api_url: api_url,
+ token: 'new-sample-token'
+ }
+ end
+
+ let(:update_params) do
+ {
+ name: 'new-name',
+ platform_kubernetes_attributes: platform_kubernetes_attributes
+ }
+ end
+
+ it 'responds with 200' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'updates platform kubernetes attributes' do
+ platform_kubernetes = cluster.platform_kubernetes
+
+ expect(cluster.name).to eq('new-name')
+ expect(platform_kubernetes.api_url).to eq('https://new-api-url.com')
+ expect(platform_kubernetes.token).to eq('new-sample-token')
+ end
+ end
+
+ context 'with a cluster that does not exist' do
+ let(:cluster_id) { 1337 }
+
+ it 'returns 404' do
+ put api("/admin/clusters/#{cluster_id}", admin_user), params: update_params
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when trying to update a project cluster via the instance cluster endpoint' do
+ it 'returns 404' do
+ put api("/admin/clusters/#{project_cluster_id}", admin_user), params: update_params
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'non-authorized user' do
+ it 'responds with 403' do
+ put api("/admin/clusters/#{cluster.id}", regular_user), params: update_params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ describe 'DELETE /admin/clusters/:cluster_id' do
+ let(:cluster_params) { { cluster_id: cluster.id } }
+
+ let_it_be(:cluster) do
+ create(:cluster, :instance, :provided_by_gcp)
+ end
+
+ context 'authorized user' do
+ before do
+ delete api("/admin/clusters/#{cluster.id}", admin_user), params: cluster_params
+ end
+
+ it 'responds with 204' do
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it 'deletes the cluster' do
+ expect(Clusters::Cluster.exists?(id: cluster.id)).to be_falsy
+ end
+
+ context 'with a cluster that does not exist' do
+ let(:cluster_id) { 1337 }
+
+ it 'returns 404' do
+ delete api("/admin/clusters/#{cluster_id}", admin_user)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when trying to update a project cluster via the instance cluster endpoint' do
+ it 'returns 404' do
+ delete api("/admin/clusters/#{project_cluster_id}", admin_user)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'non-authorized user' do
+ it 'responds with 403' do
+ delete api("/admin/clusters/#{cluster.id}", regular_user), params: cluster_params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb
new file mode 100644
index 00000000000..e5803f50474
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/todo/create_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Creating a todo for the alert' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:alert) { create(:alert_management_alert, project: project) }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: alert.iid.to_s
+ }
+ graphql_mutation(:alert_todo_create, variables) do
+ <<~QL
+ clientMutationId
+ errors
+ todo {
+ author {
+ username
+ }
+ }
+ QL
+ end
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:alert_todo_create) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'creates a todo for the current user' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['todo']['author']['username']).to eq(user.username)
+ end
+
+ context 'todo already exists' do
+ before do
+ create(:todo, :pending, project: project, user: user, target: alert)
+ end
+
+ it 'surfaces an error' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to eq(['You already have pending todo for this alert'])
+ end
+ end
+end
diff --git a/spec/requests/product_analytics/collector_app_attack_spec.rb b/spec/requests/product_analytics/collector_app_attack_spec.rb
new file mode 100644
index 00000000000..6f86e39c295
--- /dev/null
+++ b/spec/requests/product_analytics/collector_app_attack_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ProductAnalytics::CollectorApp throttle' do
+ include RackAttackSpecHelpers
+
+ include_context 'rack attack cache store'
+
+ let(:project1) { create(:project) }
+ let(:project2) { create(:project) }
+
+ before do
+ allow(ProductAnalyticsEvent).to receive(:create).and_return(true)
+ end
+
+ context 'per application id' do
+ let(:params) do
+ {
+ aid: project1.id,
+ eid: SecureRandom.uuid
+ }
+ end
+
+ it 'throttles the endpoint' do
+ # Allow requests under the rate limit.
+ 100.times do
+ expect_ok { get '/-/collector/i', params: params }
+ end
+
+ # Ensure its not related to ip address
+ random_next_ip
+
+ # Reject request over the limit
+ expect_rejection { get '/-/collector/i', params: params }
+
+ # But allows request for different aid
+ expect_ok { get '/-/collector/i', params: params.merge(aid: project2.id) }
+ end
+ end
+end
diff --git a/spec/requests/product_analytics/collector_app_spec.rb b/spec/requests/product_analytics/collector_app_spec.rb
new file mode 100644
index 00000000000..0491c2564f0
--- /dev/null
+++ b/spec/requests/product_analytics/collector_app_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'ProductAnalytics::CollectorApp' do
+ let_it_be(:project) { create(:project) }
+ let(:params) { {} }
+
+ subject { get '/-/collector/i', params: params }
+
+ RSpec.shared_examples 'not found' do
+ it 'repond with 404' do
+ expect { subject }.not_to change { ProductAnalyticsEvent.count }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'correct event params' do
+ let(:params) do
+ {
+ aid: project.id,
+ p: 'web',
+ tna: 'sp',
+ tv: 'js-2.14.0',
+ eid: SecureRandom.uuid,
+ duid: SecureRandom.uuid,
+ sid: SecureRandom.uuid,
+ vid: 4,
+ url: 'http://example.com/products/1',
+ refr: 'http://example.com/products/1',
+ lang: 'en-US',
+ cookie: true,
+ tz: 'America/Los_Angeles',
+ cs: 'UTF-8'
+ }
+ end
+
+ it 'repond with 200' do
+ expect { subject }.to change { ProductAnalyticsEvent.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'feature disabled' do
+ before do
+ stub_feature_flags(product_analytics: false)
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+
+ context 'empty event params' do
+ it_behaves_like 'not found'
+ end
+
+ context 'invalid project id in params' do
+ let(:params) do
+ {
+ aid: '-1',
+ p: 'web',
+ tna: 'sp',
+ tv: 'js-2.14.0',
+ eid: SecureRandom.uuid,
+ duid: SecureRandom.uuid,
+ sid: SecureRandom.uuid
+ }
+ end
+
+ it_behaves_like 'not found'
+ end
+end
diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb
index 0ec418d33d1..15d2f32de78 100644
--- a/spec/routing/import_routing_spec.rb
+++ b/spec/routing/import_routing_spec.rb
@@ -90,25 +90,39 @@ RSpec.describe Import::GiteaController, 'routing' do
end
end
-# status_import_gitlab GET /import/gitlab/status(.:format) import/gitlab#status
-# callback_import_gitlab GET /import/gitlab/callback(.:format) import/gitlab#callback
-# jobs_import_gitlab GET /import/gitlab/jobs(.:format) import/gitlab#jobs
-# import_gitlab POST /import/gitlab(.:format) import/gitlab#create
+# status_import_gitlab GET /import/gitlab/status(.:format) import/gitlab#status
+# callback_import_gitlab GET /import/gitlab/callback(.:format) import/gitlab#callback
+# realtime_changes_import_gitlab GET /import/gitlab/realtime_changes(.:format) import/gitlab#realtime_changes
+# import_gitlab POST /import/gitlab(.:format) import/gitlab#create
RSpec.describe Import::GitlabController, 'routing' do
it_behaves_like 'importer routing' do
let(:except_actions) { [:new] }
let(:provider) { 'gitlab' }
+ let(:is_realtime) { true }
end
end
-# status_import_bitbucket GET /import/bitbucket/status(.:format) import/bitbucket#status
-# callback_import_bitbucket GET /import/bitbucket/callback(.:format) import/bitbucket#callback
-# jobs_import_bitbucket GET /import/bitbucket/jobs(.:format) import/bitbucket#jobs
-# import_bitbucket POST /import/bitbucket(.:format) import/bitbucket#create
+# status_import_bitbucket GET /import/bitbucket/status(.:format) import/bitbucket#status
+# callback_import_bitbucket GET /import/bitbucket/callback(.:format) import/bitbucket#callback
+# realtime_changes_import_bitbucket GET /import/bitbucket/realtime_changes(.:format) import/bitbucket#realtime_changes
+# import_bitbucket POST /import/bitbucket(.:format) import/bitbucket#create
RSpec.describe Import::BitbucketController, 'routing' do
it_behaves_like 'importer routing' do
let(:except_actions) { [:new] }
let(:provider) { 'bitbucket' }
+ let(:is_realtime) { true }
+ end
+end
+
+# status_import_bitbucket_server GET /import/bitbucket_server/status(.:format) import/bitbucket_server#status
+# callback_import_bitbucket_server GET /import/bitbucket_server/callback(.:format) import/bitbucket_server#callback
+# realtime_changes_import_bitbucket_server GET /import/bitbucket_server/realtime_changes(.:format) import/bitbucket_server#realtime_changes
+# new_import_bitbucket_server GET /import/bitbucket_server/new(.:format) import/bitbucket_server#new
+# import_bitbucket_server POST /import/bitbucket_server(.:format) import/bitbucket_server#create
+RSpec.describe Import::BitbucketServerController, 'routing' do
+ it_behaves_like 'importer routing' do
+ let(:provider) { 'bitbucket_server' }
+ let(:is_realtime) { true }
end
end
@@ -138,17 +152,18 @@ RSpec.describe Import::GoogleCodeController, 'routing' do
end
end
-# status_import_fogbugz GET /import/fogbugz/status(.:format) import/fogbugz#status
-# callback_import_fogbugz POST /import/fogbugz/callback(.:format) import/fogbugz#callback
-# jobs_import_fogbugz GET /import/fogbugz/jobs(.:format) import/fogbugz#jobs
-# new_user_map_import_fogbugz GET /import/fogbugz/user_map(.:format) import/fogbugz#new_user_map
-# create_user_map_import_fogbugz POST /import/fogbugz/user_map(.:format) import/fogbugz#create_user_map
-# import_fogbugz POST /import/fogbugz(.:format) import/fogbugz#create
-# new_import_fogbugz GET /import/fogbugz/new(.:format) import/fogbugz#new
+# status_import_fogbugz GET /import/fogbugz/status(.:format) import/fogbugz#status
+# callback_import_fogbugz POST /import/fogbugz/callback(.:format) import/fogbugz#callback
+# realtime_changes_import_fogbugz GET /import/fogbugz/realtime_changes(.:format) import/fogbugz#realtime_changes
+# new_user_map_import_fogbugz GET /import/fogbugz/user_map(.:format) import/fogbugz#new_user_map
+# create_user_map_import_fogbugz POST /import/fogbugz/user_map(.:format) import/fogbugz#create_user_map
+# import_fogbugz POST /import/fogbugz(.:format) import/fogbugz#create
+# new_import_fogbugz GET /import/fogbugz/new(.:format) import/fogbugz#new
RSpec.describe Import::FogbugzController, 'routing' do
it_behaves_like 'importer routing' do
let(:except_actions) { [:callback] }
let(:provider) { 'fogbugz' }
+ let(:is_realtime) { true }
end
it 'to #callback' do
diff --git a/spec/services/alert_management/alerts/todo/create_service_spec.rb b/spec/services/alert_management/alerts/todo/create_service_spec.rb
new file mode 100644
index 00000000000..e3d9de8b4df
--- /dev/null
+++ b/spec/services/alert_management/alerts/todo/create_service_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AlertManagement::Alerts::Todo::CreateService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:alert) { create(:alert_management_alert) }
+
+ let(:current_user) { user }
+
+ describe '#execute' do
+ subject(:result) { AlertManagement::Alerts::Todo::CreateService.new(alert, current_user).execute }
+
+ shared_examples 'permissions error' do
+ it 'returns an error', :aggregate_failures do
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('You have insufficient permissions to create a Todo for this alert')
+ expect(result.payload[:todo]).to be(nil)
+ expect(result.payload[:alert]).to be(alert)
+ end
+ end
+
+ context 'when the user is anonymous' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'permissions error'
+ end
+
+ context 'when the user does not have permission' do
+ it_behaves_like 'permissions error'
+ end
+
+ context 'when user has permission' do
+ before do
+ alert.project.add_developer(user)
+ end
+
+ it 'creates a todo' do
+ expect { result }.to change { Todo.count }.by(1)
+ end
+
+ it 'returns the alert and todo in the payload', :aggregate_failures do
+ expect(result.success?).to be(true)
+ expect(result.payload[:alert][:id]).to be(alert.id)
+ expect(result.payload[:todo][:id]).to be(Todo.last.id)
+ end
+
+ context 'when the user has a marked todo for the alert' do
+ let_it_be(:todo_params) do
+ { project: alert.project,
+ target: alert,
+ user: user,
+ action: Todo::MARKED }
+ end
+
+ context 'when todo is pending' do
+ before_all do
+ create(:todo, :pending, **todo_params)
+ end
+
+ it 'does not create a todo' do
+ expect { result }.not_to change { Todo.count }
+ end
+
+ it 'returns an error', :aggregate_failures do
+ expect(result.error?).to be(true)
+ expect(result.message).to be('You already have pending todo for this alert')
+ expect(result.payload[:todo]).to be(nil)
+ expect(result.payload[:alert]).to be(alert)
+ end
+ end
+
+ context 'when todo is done' do
+ before do
+ create(:todo, :done, **todo_params)
+ end
+
+ it { expect(result.success?).to be(true) }
+ it { expect { result }.to change { Todo.count }.by(1) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
index 092742276d3..3bf59f6a2d1 100644
--- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
@@ -69,6 +69,7 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do
before do
allow(merge_request)
.to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
+ expect(MailScheduler::NotificationServiceWorker).to receive(:perform_async).with('merge_when_pipeline_succeeds', merge_request, user).once
service.execute(merge_request)
end
@@ -90,6 +91,18 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do
end
end
+ context 'without feature enabled' do
+ it 'does not send notification' do
+ stub_feature_flags(mwps_notification: false)
+
+ allow(merge_request)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
+ expect(MailScheduler::NotificationServiceWorker).not_to receive(:perform_async)
+
+ service.execute(merge_request)
+ end
+ end
+
context 'already approved' do
let(:service) { described_class.new(project, user, should_remove_source_branch: true) }
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
@@ -106,6 +119,7 @@ RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService do
it 'updates the merge params' do
expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds)
+ expect(MailScheduler::NotificationServiceWorker).not_to receive(:perform_async).with('merge_when_pipeline_succeeds', any_args)
service.execute(mr_merge_if_green_enabled)
expect(mr_merge_if_green_enabled.merge_params).to have_key('should_remove_source_branch')
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 7e4b61ed1b7..d10ed7d6640 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -290,18 +290,6 @@ RSpec.describe EventCreateService do
let_it_be(:design) { create(:design, project: project) }
let_it_be(:author) { user }
- shared_examples 'feature flag gated multiple event creation' do
- context 'the feature flag is off' do
- before do
- stub_feature_flags(design_activity_events: false)
- end
-
- specify { expect(result).to be_empty }
- specify { expect { result }.not_to change { Event.count } }
- specify { expect { result }.not_to exceed_query_limit(0) }
- end
- end
-
describe '#save_designs' do
let_it_be(:updated) { create_list(:design, 5) }
let_it_be(:created) { create_list(:design, 3) }
@@ -326,8 +314,6 @@ RSpec.describe EventCreateService do
expect(events.map(&:design)).to match_array(updated)
end
- it_behaves_like 'feature flag gated multiple event creation'
-
it 'records the event in the event counter' do
stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true)
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
@@ -356,8 +342,6 @@ RSpec.describe EventCreateService do
expect(events.map(&:design)).to match_array(designs)
end
- it_behaves_like 'feature flag gated multiple event creation'
-
it 'records the event in the event counter' do
stub_feature_flags(Gitlab::UsageDataCounters::TrackUniqueActions::FEATURE_FLAG => true)
counter_class = Gitlab::UsageDataCounters::TrackUniqueActions
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 9c837019d37..2fe7a46de4b 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2023,6 +2023,26 @@ RSpec.describe NotificationService, :mailer do
let(:notification_trigger) { notification.resolve_all_discussions(merge_request, @u_disabled) }
end
end
+
+ describe '#merge_when_pipeline_succeeds' do
+ it 'send notification that merge will happen when pipeline succeeds' do
+ notification.merge_when_pipeline_succeeds(merge_request, assignee)
+ should_email(merge_request.author)
+ should_email(@u_watcher)
+ should_email(@subscriber)
+ end
+
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { merge_request }
+ let(:notification_trigger) { notification.merge_when_pipeline_succeeds(merge_request, @u_disabled) }
+ end
+
+ it_behaves_like 'project emails are disabled' do
+ let(:notification_target) { merge_request }
+ let(:notification_trigger) { notification.merge_when_pipeline_succeeds(merge_request, @u_disabled) }
+ end
+ end
end
describe 'Projects', :deliver_mails_inline do
diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb
index f9ddf954cd5..6209294f4ce 100644
--- a/spec/services/resource_events/merge_into_notes_service_spec.rb
+++ b/spec/services/resource_events/merge_into_notes_service_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe ResourceEvents::MergeIntoNotesService do
event = create_event(created_at: 1.day.ago)
notes = described_class.new(resource, user,
- last_fetched_at: 2.days.ago.to_i).execute
+ last_fetched_at: 2.days.ago).execute
expect(notes.count).to eq 1
expect(notes.first.discussion_id).to eq event.discussion_id
diff --git a/spec/support/helpers/rack_attack_spec_helpers.rb b/spec/support/helpers/rack_attack_spec_helpers.rb
index e0cedb5a57b..65082ec690f 100644
--- a/spec/support/helpers/rack_attack_spec_helpers.rb
+++ b/spec/support/helpers/rack_attack_spec_helpers.rb
@@ -30,4 +30,16 @@ module RackAttackSpecHelpers
expect(response).to have_gitlab_http_status(:too_many_requests)
end
+
+ def expect_ok(&block)
+ yield
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ def random_next_ip
+ allow_next_instance_of(Rack::Attack::Request) do |instance|
+ allow(instance).to receive(:ip).and_return(FFaker::Internet.ip_v4_address)
+ end
+ end
end
diff --git a/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb
deleted file mode 100644
index 88ad1f6cde2..00000000000
--- a/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'import controller with new_import_ui feature flag' do
- include ImportSpecHelper
-
- context 'with new_import_ui feature flag enabled' do
- let(:group) { create(:group) }
-
- before do
- stub_feature_flags(new_import_ui: true)
- group.add_owner(user)
- end
-
- it "returns variables for json request" do
- project = create(:project, import_type: provider_name, creator_id: user.id)
- stub_client(client_repos_field => [repo])
-
- get :status, format: :json
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
- expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id)
- expect(json_response.dig("namespaces", 0, "id")).to eq(group.id)
- end
-
- it "does not show already added project" do
- project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source)
- stub_client(client_repos_field => [repo])
-
- get :status, format: :json
-
- expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
- expect(json_response.dig("provider_repos")).to eq([])
- end
- end
-end
diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
new file mode 100644
index 00000000000..ecb9abc5c46
--- /dev/null
+++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'import controller status' do
+ include ImportSpecHelper
+
+ let(:group) { create(:group) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ it "returns variables for json request" do
+ project = create(:project, import_type: provider_name, creator_id: user.id)
+ stub_client(client_repos_field => [repo])
+
+ get :status, format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
+ expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id)
+ expect(json_response.dig("namespaces", 0, "id")).to eq(group.id)
+ end
+
+ it "does not show already added project" do
+ project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source)
+ stub_client(client_repos_field => [repo])
+
+ get :status, format: :json
+
+ expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
+ expect(json_response.dig("provider_repos")).to eq([])
+ end
+end