Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorJohn T Skarbek <jskarbek@gitlab.com>2019-04-29 17:29:07 +0300
committerJohn T Skarbek <jskarbek@gitlab.com>2019-04-29 17:29:07 +0300
commit17e8e8d3f43dda6114523a1fd15096cae3bd71d4 (patch)
tree4a0f35a0bf6199fb6823d13f4174fe87b446fc98 /spec
parent076d199d2af03be9c41962c9e5203a02ddef691d (diff)
parent41fed29a60b10ded9130c0f61119965ffcd28b88 (diff)
Merge remote-tracking branch 'origin/master'
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb45
-rw-r--r--spec/controllers/projects/settings/operations_controller_spec.rb224
-rw-r--r--spec/controllers/search_controller_spec.rb24
-rw-r--r--spec/factories/deployments.rb2
-rw-r--r--spec/factories/project_metrics_settings.rb8
-rw-r--r--spec/features/admin/admin_settings_spec.rb46
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb2
-rw-r--r--spec/features/protected_branches_spec.rb26
-rw-r--r--spec/features/protected_tags_spec.rb13
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb36
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml36
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json13
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json20
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json17
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json20
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js47
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js73
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js134
-rw-r--r--spec/frontend/clusters/services/mock_data.js1
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js43
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js185
-rw-r--r--spec/frontend/import_projects/components/imported_project_table_row_spec.js (renamed from spec/javascripts/import_projects/components/imported_project_table_row_spec.js)25
-rw-r--r--spec/frontend/import_projects/components/provider_repo_table_row_spec.js (renamed from spec/javascripts/import_projects/components/provider_repo_table_row_spec.js)90
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js (renamed from spec/javascripts/import_projects/store/actions_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js (renamed from spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js172
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_warning_spec.js (renamed from spec/javascripts/vue_shared/components/issue/issue_warning_spec.js)10
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js (renamed from spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js)12
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/system_note_spec.js)7
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb155
-rw-r--r--spec/javascripts/fixtures/static/images/green_box.png (renamed from spec/javascripts/fixtures/images/green_box.png)bin1306 -> 1306 bytes
-rw-r--r--spec/javascripts/fixtures/static/images/one_white_pixel.png (renamed from spec/javascripts/fixtures/one_white_pixel.png)bin68 -> 68 bytes
-rw-r--r--spec/javascripts/fixtures/static/images/red_box.png (renamed from spec/javascripts/fixtures/images/red_box.png)bin1305 -> 1305 bytes
-rw-r--r--spec/javascripts/fixtures/static/projects.json (renamed from spec/javascripts/fixtures/projects.json)0
-rw-r--r--spec/javascripts/gl_dropdown_spec.js4
-rw-r--r--spec/javascripts/import_projects/components/import_projects_table_spec.js188
-rw-r--r--spec/javascripts/test_constants.js6
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js16
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js234
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js4
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb14
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb36
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb6
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb42
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml7
-rw-r--r--spec/lib/gitlab/metrics/dashboard/processor_spec.rb94
-rw-r--r--spec/lib/gitlab/metrics/dashboard/service_spec.rb42
-rw-r--r--spec/lib/gitlab/profiler_spec.rb6
-rw-r--r--spec/models/application_setting_spec.rb14
-rw-r--r--spec/models/ci/build_spec.rb4
-rw-r--r--spec/models/clusters/applications/runner_spec.rb4
-rw-r--r--spec/models/deployment_spec.rb27
-rw-r--r--spec/models/project_metrics_setting_spec.rb55
-rw-r--r--spec/models/project_services/chat_message/deployment_message_spec.rb150
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb6
-rw-r--r--spec/models/repository_spec.rb65
-rw-r--r--spec/rubocop/cop/include_action_view_context_spec.rb45
-rw-r--r--spec/services/projects/operations/update_service_spec.rb50
-rw-r--r--spec/services/update_deployment_service_spec.rb1
-rw-r--r--spec/support/helpers/graphql_helpers.rb13
-rw-r--r--spec/support/protected_branch_helpers.rb30
-rw-r--r--spec/support/protected_tag_helpers.rb18
-rw-r--r--spec/support/shared_examples/models/chat_service_spec.rb6
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb8
-rw-r--r--spec/workers/build_success_worker_spec.rb1
-rw-r--r--spec/workers/deployments/finished_worker_spec.rb39
70 files changed, 1921 insertions, 819 deletions
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 75158f2e8e0..a62422d0229 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -342,11 +342,9 @@ describe Projects::EnvironmentsController do
end
context 'when environment has no metrics' do
- before do
- expect(environment).to receive(:metrics).and_return(nil)
- end
-
it 'returns a metrics page' do
+ expect(environment).not_to receive(:metrics)
+
get :metrics, params: environment_params
expect(response).to be_ok
@@ -354,6 +352,8 @@ describe Projects::EnvironmentsController do
context 'when requesting metrics as JSON' do
it 'returns a metrics JSON document' do
+ expect(environment).to receive(:metrics).and_return(nil)
+
get :metrics, params: environment_params(format: :json)
expect(response).to have_gitlab_http_status(204)
@@ -461,6 +461,43 @@ describe Projects::EnvironmentsController do
end
end
+ describe 'metrics_dashboard' do
+ context 'when prometheus endpoint is disabled' do
+ before do
+ stub_feature_flags(environment_metrics_use_prometheus_endpoint: false)
+ end
+
+ it 'responds with status code 403' do
+ get :metrics_dashboard, params: environment_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when prometheus endpoint is enabled' do
+ it 'returns a json representation of the environment dashboard' do
+ get :metrics_dashboard, params: environment_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.keys).to contain_exactly('dashboard', 'status')
+ expect(json_response['dashboard']).to be_an_instance_of(Hash)
+ end
+
+ context 'when the dashboard could not be provided' do
+ before do
+ allow(YAML).to receive(:safe_load).and_return({})
+ end
+
+ it 'returns an error response' do
+ get :metrics_dashboard, params: environment_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ expect(json_response.keys).to contain_exactly('message', 'status', 'http_status')
+ end
+ end
+ end
+ end
+
describe 'GET #search' do
before do
create(:environment, name: 'staging', project: project)
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index 02a392f23c2..aa9cd41ed19 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -11,15 +11,118 @@ describe Projects::Settings::OperationsController do
project.add_maintainer(user)
end
- context 'error tracking' do
- describe 'GET #show' do
- it 'renders show template' do
+ shared_examples 'PATCHable' do
+ let(:operations_update_service) { instance_double(::Projects::Operations::UpdateService) }
+ let(:operations_url) { project_settings_operations_url(project) }
+
+ let(:permitted_params) do
+ ActionController::Parameters.new(params).permit!
+ end
+
+ context 'format json' do
+ context 'when update succeeds' do
+ it 'returns success status' do
+ stub_operations_update_service_returning(status: :success)
+
+ patch :update,
+ params: project_params(project, params),
+ format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq('status' => 'success')
+ expect(flash[:notice]).to eq('Your changes have been saved')
+ end
+ end
+
+ context 'when update fails' do
+ it 'returns error' do
+ stub_operations_update_service_returning(
+ status: :error,
+ message: 'error message'
+ )
+
+ patch :update,
+ params: project_params(project, params),
+ format: :json
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('error message')
+ end
+ end
+ end
+
+ private
+
+ def stub_operations_update_service_returning(return_value = {})
+ expect(::Projects::Operations::UpdateService)
+ .to receive(:new).with(project, user, permitted_params)
+ .and_return(operations_update_service)
+ expect(operations_update_service).to receive(:execute)
+ .and_return(return_value)
+ end
+ end
+
+ describe 'GET #show' do
+ it 'renders show template' do
+ get :show, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+
+ context 'with insufficient permissions' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'renders 404' do
+ get :show, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'as an anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to signup page' do
get :show, params: project_params(project)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ describe 'PATCH #update' do
+ context 'with insufficient permissions' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'renders 404' do
+ patch :update, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'as an anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to signup page' do
+ patch :update, params: project_params(project)
+
+ expect(response).to redirect_to(new_user_session_path)
end
+ end
+ end
+ context 'error tracking' do
+ describe 'GET #show' do
context 'with existing setting' do
let!(:error_tracking_setting) do
create(:project_error_tracking_setting, project: project)
@@ -40,37 +143,10 @@ describe Projects::Settings::OperationsController do
expect(controller.helpers.error_tracking_setting).to be_new_record
end
end
-
- context 'with insufficient permissions' do
- before do
- project.add_reporter(user)
- end
-
- it 'renders 404' do
- get :show, params: project_params(project)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'as an anonymous user' do
- before do
- sign_out(user)
- end
-
- it 'redirects to signup page' do
- get :show, params: project_params(project)
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
end
describe 'PATCH #update' do
- let(:operations_update_service) { spy(:operations_update_service) }
- let(:operations_url) { project_settings_operations_url(project) }
-
- let(:error_tracking_params) do
+ let(:params) do
{
error_tracking_setting_attributes: {
enabled: '1',
@@ -86,79 +162,21 @@ describe Projects::Settings::OperationsController do
}
end
- let(:error_tracking_permitted) do
- ActionController::Parameters.new(error_tracking_params).permit!
- end
-
- context 'format json' do
- context 'when update succeeds' do
- before do
- stub_operations_update_service_returning(status: :success)
- end
-
- it 'returns success status' do
- patch :update,
- params: project_params(project, error_tracking_params),
- format: :json
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq('status' => 'success')
- expect(flash[:notice]).to eq('Your changes have been saved')
- end
- end
-
- context 'when update fails' do
- before do
- stub_operations_update_service_returning(
- status: :error,
- message: 'error message'
- )
- end
-
- it 'returns error' do
- patch :update,
- params: project_params(project, error_tracking_params),
- format: :json
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).not_to be_nil
- end
- end
- end
-
- context 'with insufficient permissions' do
- before do
- project.add_reporter(user)
- end
-
- it 'renders 404' do
- patch :update, params: project_params(project)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'as an anonymous user' do
- before do
- sign_out(user)
- end
-
- it 'redirects to signup page' do
- patch :update, params: project_params(project)
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
+ it_behaves_like 'PATCHable'
end
+ end
- private
+ context 'metrics dashboard setting' do
+ describe 'PATCH #update' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: 'https://gitlab.com'
+ }
+ }
+ end
- def stub_operations_update_service_returning(return_value = {})
- expect(::Projects::Operations::UpdateService)
- .to receive(:new).with(project, user, error_tracking_permitted)
- .and_return(operations_update_service)
- expect(operations_update_service).to receive(:execute)
- .and_return(return_value)
+ it_behaves_like 'PATCHable'
end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 9c60f0fcd4d..4634d1d4bb3 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -11,6 +11,30 @@ describe SearchController do
sign_in(user)
end
+ context 'uses the right partials depending on scope' do
+ using RSpec::Parameterized::TableSyntax
+ render_views
+
+ set(:project) { create(:project, :public, :repository, :wiki_repo) }
+
+ subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
+
+ where(:partial, :scope) do
+ '_blob' | :blobs
+ '_wiki_blob' | :wiki_blobs
+ '_commit' | :commits
+ end
+
+ with_them do
+ it do
+ project_wiki = create(:project_wiki, project: project, user: user)
+ create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' })
+
+ expect(subject).to render_template("search/results/#{partial}")
+ end
+ end
+ end
+
it 'finds issue comments' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 011c98599a3..db438ad32d3 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -1,6 +1,6 @@
FactoryBot.define do
factory :deployment, class: Deployment do
- sha '97de212e80737a608d939f648d959671fb0a0142'
+ sha 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
ref 'master'
tag false
user nil
diff --git a/spec/factories/project_metrics_settings.rb b/spec/factories/project_metrics_settings.rb
new file mode 100644
index 00000000000..234753f9b87
--- /dev/null
+++ b/spec/factories/project_metrics_settings.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_metrics_setting, class: ProjectMetricsSetting do
+ project
+ external_dashboard_url 'https://grafana.com'
+ end
+end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 04f39b807d7..f9950b5b03f 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -230,6 +230,13 @@ describe 'Admin updates settings' do
expect(find_field('Username').value).to eq 'test_user'
expect(find('#service_push_channel').value).to eq '#test_channel'
end
+
+ it 'defaults Deployment events to false for chat notification template settings' do
+ first(:link, 'Service Templates').click
+ click_link 'Slack notifications'
+
+ expect(find_field('Deployment')).not_to be_checked
+ end
end
context 'CI/CD page' do
@@ -368,15 +375,50 @@ describe 'Admin updates settings' do
expect(Gitlab::CurrentSettings.pages_domain_verification_enabled?).to be_truthy
expect(page).to have_content "Application settings saved successfully"
end
+
+ context 'When pages_auto_ssl is enabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: true)
+ visit preferences_admin_application_settings_path
+ end
+
+ it "Change Pages Let's Encrypt settings" do
+ page.within('.as-pages') do
+ fill_in 'Email', with: 'my@test.example.com'
+ check "I have read and agree to the Let's Encrypt Terms of Service"
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.lets_encrypt_notification_email).to eq 'my@test.example.com'
+ expect(Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted).to eq true
+ end
+ end
+
+ context 'When pages_auto_ssl is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+ visit preferences_admin_application_settings_path
+ end
+
+ it "Doesn't show Let's Encrypt options" do
+ page.within('.as-pages') do
+ expect(page).not_to have_content('Email')
+ end
+ end
+ end
end
def check_all_events
page.check('Active')
page.check('Push')
- page.check('Tag push')
- page.check('Note')
page.check('Issue')
+ page.check('Confidential issue')
page.check('Merge request')
+ page.check('Note')
+ page.check('Confidential note')
+ page.check('Tag push')
page.check('Pipeline')
+ page.check('Wiki page')
+ page.check('Deployment')
end
end
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
index 8eaccfc0949..cc04798248c 100644
--- a/spec/features/issuables/markdown_references/jira_spec.rb
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -1,6 +1,6 @@
require "rails_helper"
-describe "Jira", :js do
+describe "Jira", :js, :quarantine do
let(:user) { create(:user) }
let(:actual_project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project) }
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 0aff916ec83..0dbff5a2701 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Protected Branches', :js do
+ include ProtectedBranchHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
@@ -150,27 +152,11 @@ describe 'Protected Branches', :js do
end
describe "access control" do
- include_examples "protected branches > access control > CE"
- end
- end
-
- def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").click
- find(".dropdown-input-field").set(branch_name)
- click_on("Create wildcard #{branch_name}")
- end
-
- def set_defaults
- find(".js-allowed-to-merge").click
- within('.qa-allowed-to-merge-dropdown') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
- find(".js-allowed-to-push").click
- within('.qa-allowed-to-push-dropdown') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
+ include_examples "protected branches > access control > CE"
end
end
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index c8e92cd1c07..652542b1719 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Protected Tags', :js do
+ include ProtectedTagHelpers
+
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :repository) }
@@ -8,13 +10,6 @@ describe 'Protected Tags', :js do
sign_in(user)
end
- def set_protected_tag_name(tag_name)
- find(".js-protected-tag-select").click
- find(".dropdown-input-field").set(tag_name)
- click_on("Create wildcard #{tag_name}")
- find('.protected-tags-dropdown .dropdown-menu', visible: false)
- end
-
describe "explicit protected tags" do
it "allows creating explicit protected tags" do
visit project_protected_tags_path(project)
@@ -92,6 +87,10 @@ describe 'Protected Tags', :js do
end
describe "access control" do
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
+
include_examples "protected tags > access control > CE"
end
end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 7225ca65492..6d4facd0649 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -14,22 +14,36 @@ describe 'User searches for wiki pages', :js do
include_examples 'top right search form'
- it 'finds a page' do
- find('.js-search-project-dropdown').click
+ shared_examples 'search wiki blobs' do
+ it 'finds a page' do
+ find('.js-search-project-dropdown').click
- page.within('.project-filter') do
- click_link(project.full_name)
- end
+ page.within('.project-filter') do
+ click_link(project.full_name)
+ end
+
+ fill_in('dashboard_search', with: 'content')
+ find('.btn-search').click
+
+ page.within('.search-filter') do
+ click_link('Wiki')
+ end
- fill_in('dashboard_search', with: 'content')
- find('.btn-search').click
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug))
+ end
+ end
+ end
- page.within('.search-filter') do
- click_link('Wiki')
+ context 'when searching by content' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'content' }
end
+ end
- page.within('.results') do
- expect(find(:css, '.search-results')).to have_link(wiki_page.title)
+ context 'when searching by title' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'test_wiki' }
end
end
end
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
new file mode 100644
index 00000000000..c2d3d3d8aca
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
@@ -0,0 +1,36 @@
+dashboard: 'Test Dashboard'
+priority: 1
+panel_groups:
+- group: Group A
+ priority: 10
+ panels:
+ - title: "Super Chart A1"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 1
+ metrics:
+ - id: metric_a1
+ query_range: 'query'
+ unit: unit
+ label: Legend Label
+ - title: "Super Chart A2"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 2
+ metrics:
+ - id: metric_a2
+ query_range: 'query'
+ label: Legend Label
+ unit: unit
+- group: Group B
+ priority: 1
+ panels:
+ - title: "Super Chart B"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 1
+ metrics:
+ - id: metric_b
+ query_range: 'query'
+ unit: unit
+ label: Legend Label
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
new file mode 100644
index 00000000000..1ee1205e29a
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
@@ -0,0 +1,13 @@
+{
+ "type": "object",
+ "required": ["dashboard", "priority", "panel_groups"],
+ "properties": {
+ "dashboard": { "type": "string" },
+ "priority": { "type": "number" },
+ "panel_groups": {
+ "type": "array",
+ "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
new file mode 100644
index 00000000000..2d0af57ec2c
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "unit",
+ "label"
+ ],
+ "oneOf": [
+ { "required": ["query"] },
+ { "required": ["query_range"] }
+ ],
+ "properties": {
+ "id": { "type": "string" },
+ "query_range": { "type": "string" },
+ "query": { "type": "string" },
+ "unit": { "type": "string" },
+ "label": { "type": "string" },
+ "track": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
new file mode 100644
index 00000000000..d7a390adcdc
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
@@ -0,0 +1,17 @@
+{
+ "type": "object",
+ "required": [
+ "group",
+ "priority",
+ "panels"
+ ],
+ "properties": {
+ "group": { "type": "string" },
+ "priority": { "type": "number" },
+ "panels": {
+ "type": "array",
+ "items": { "$ref": "panels.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
new file mode 100644
index 00000000000..1548daacd64
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "title",
+ "y_label",
+ "weight",
+ "metrics"
+ ],
+ "properties": {
+ "title": { "type": "string" },
+ "type": { "type": "string" },
+ "y_label": { "type": "string" },
+ "weight": { "type": "number" },
+ "metrics": {
+ "type": "array",
+ "items": { "$ref": "metrics.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index 33a35069004..5103cb4f69f 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,16 +1,13 @@
import Clusters from '~/clusters/clusters_bundle';
-import {
- REQUEST_SUBMITTED,
- REQUEST_FAILURE,
- APPLICATION_STATUS,
- INGRESS_DOMAIN_SUFFIX,
-} from '~/clusters/constants';
+import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX } from '~/clusters/constants';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import $ from 'jquery';
+const { INSTALLING, INSTALLABLE, INSTALLED, NOT_INSTALLABLE } = APPLICATION_STATUS;
+
describe('Clusters', () => {
setTestTimeout(1000);
@@ -93,7 +90,7 @@ describe('Clusters', () => {
it('does not show alert when things transition from initial null state to something', () => {
cluster.checkForNewInstalls(INITIAL_APP_MAP, {
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Helm Tiller' },
+ helm: { status: INSTALLABLE, title: 'Helm Tiller' },
});
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
@@ -105,11 +102,11 @@ describe('Clusters', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' },
+ helm: { status: INSTALLING, title: 'Helm Tiller' },
},
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' },
+ helm: { status: INSTALLED, title: 'Helm Tiller' },
},
);
@@ -125,13 +122,13 @@ describe('Clusters', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' },
- ingress: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Ingress' },
+ helm: { status: INSTALLING, title: 'Helm Tiller' },
+ ingress: { status: INSTALLABLE, title: 'Ingress' },
},
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' },
- ingress: { status: APPLICATION_STATUS.INSTALLED, title: 'Ingress' },
+ helm: { status: INSTALLED, title: 'Helm Tiller' },
+ ingress: { status: INSTALLED, title: 'Ingress' },
},
);
@@ -218,11 +215,11 @@ describe('Clusters', () => {
it('tries to install helm', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+ cluster.store.state.applications.helm.status = INSTALLABLE;
cluster.installApplication({ id: 'helm' });
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
});
@@ -230,11 +227,11 @@ describe('Clusters', () => {
it('tries to install ingress', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
+ cluster.store.state.applications.ingress.status = INSTALLABLE;
cluster.installApplication({ id: 'ingress' });
- expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.ingress.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
});
@@ -242,11 +239,11 @@ describe('Clusters', () => {
it('tries to install runner', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
+ cluster.store.state.applications.runner.status = INSTALLABLE;
cluster.installApplication({ id: 'runner' });
- expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.runner.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
});
@@ -254,13 +251,12 @@ describe('Clusters', () => {
it('tries to install jupyter', () => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
cluster.installApplication({
id: 'jupyter',
params: { hostname: cluster.store.state.applications.jupyter.hostname },
});
- expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUBMITTED);
+ cluster.store.state.applications.jupyter.status = INSTALLABLE;
expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', {
hostname: cluster.store.state.applications.jupyter.hostname,
@@ -272,16 +268,18 @@ describe('Clusters', () => {
.spyOn(cluster.service, 'installApplication')
.mockRejectedValueOnce(new Error('STUBBED ERROR'));
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+ cluster.store.state.applications.helm.status = INSTALLABLE;
const promise = cluster.installApplication({ id: 'helm' });
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalled();
return promise.then(() => {
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE);
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE);
+ expect(cluster.store.state.applications.helm.installFailed).toBe(true);
+
expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
});
});
@@ -315,7 +313,6 @@ describe('Clusters', () => {
});
describe('toggleIngressDomainHelpText', () => {
- const { INSTALLED, INSTALLABLE, NOT_INSTALLABLE } = APPLICATION_STATUS;
let ingressPreviousState;
let ingressNewState;
diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index 038d2be9e98..17273b7d5b1 100644
--- a/spec/frontend/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import eventHub from '~/clusters/event_hub';
-import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '~/clusters/constants';
+import { APPLICATION_STATUS } from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
@@ -80,17 +80,6 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(false);
});
- it('has loading "Installing" when APPLICATION_STATUS.SCHEDULED', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.SCHEDULED,
- });
-
- expect(vm.installButtonLabel).toEqual('Installing');
- expect(vm.installButtonLoading).toEqual(true);
- expect(vm.installButtonDisabled).toEqual(true);
- });
-
it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
@@ -102,18 +91,6 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true);
});
- it('has loading "Installing" when REQUEST_SUBMITTED', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_SUBMITTED,
- });
-
- expect(vm.installButtonLabel).toEqual('Installing');
- expect(vm.installButtonLoading).toEqual(true);
- expect(vm.installButtonDisabled).toEqual(true);
- });
-
it('has disabled "Installed" when application is installed and not uninstallable', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
@@ -139,10 +116,11 @@ describe('Application Row', () => {
expect(installBtn).toBe(null);
});
- it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => {
+ it('has enabled "Install" when install fails', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.ERROR,
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
});
expect(vm.installButtonLabel).toEqual('Install');
@@ -154,7 +132,6 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_FAILURE,
});
expect(vm.installButtonLabel).toEqual('Install');
@@ -246,15 +223,15 @@ describe('Application Row', () => {
expect(upgradeBtn.innerHTML).toContain('Upgrade');
});
- it('has enabled "Retry update" when APPLICATION_STATUS.UPDATE_ERRORED', () => {
+ it('has enabled "Retry update" when update process fails', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
expect(upgradeBtn).not.toBe(null);
- expect(vm.upgradeFailed).toBe(true);
expect(upgradeBtn.innerHTML).toContain('Retry update');
});
@@ -274,7 +251,8 @@ describe('Application Row', () => {
jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ upgradeAvailable: true,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
@@ -303,7 +281,8 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
title: 'GitLab Runner',
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
});
const failureMessage = vm.$el.querySelector(
'.js-cluster-application-upgrade-failure-message',
@@ -314,6 +293,21 @@ describe('Application Row', () => {
'Update failed. Please check the logs and try again.',
);
});
+
+ it('displays a success toast message if application upgrade was successful', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ title: 'GitLab Runner',
+ updateSuccessful: false,
+ });
+
+ vm.$toast = { show: jest.fn() };
+ vm.updateSuccessful = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner upgraded successfully.');
+ });
+ });
});
describe('Version', () => {
@@ -321,7 +315,8 @@ describe('Application Row', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateSuccessful: true,
version,
});
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
@@ -337,7 +332,8 @@ describe('Application Row', () => {
const chartRepo = 'https://gitlab.com/charts/gitlab-runner';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateSuccessful: true,
chartRepo,
version,
});
@@ -351,7 +347,8 @@ describe('Application Row', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
version,
});
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
@@ -367,7 +364,6 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: null,
- requestStatus: null,
});
const generalErrorMessage = vm.$el.querySelector(
'.js-cluster-application-general-error-message',
@@ -376,12 +372,13 @@ describe('Application Row', () => {
expect(generalErrorMessage).toBeNull();
});
- it('shows status reason when APPLICATION_STATUS.ERROR', () => {
+ it('shows status reason when install fails', () => {
const statusReason = 'We broke it 0.0';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.ERROR,
statusReason,
+ installFailed: true,
});
const generalErrorMessage = vm.$el.querySelector(
'.js-cluster-application-general-error-message',
@@ -402,7 +399,7 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_FAILURE,
+ installFailed: true,
requestReason,
});
const generalErrorMessage = vm.$el.querySelector(
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
new file mode 100644
index 00000000000..e74b7910572
--- /dev/null
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -0,0 +1,134 @@
+import transitionApplicationState from '~/clusters/services/application_state_machine';
+import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT } from '~/clusters/constants';
+
+const {
+ NO_STATUS,
+ SCHEDULED,
+ NOT_INSTALLABLE,
+ INSTALLABLE,
+ INSTALLING,
+ INSTALLED,
+ ERROR,
+ UPDATING,
+ UPDATED,
+ UPDATE_ERRORED,
+} = APPLICATION_STATUS;
+
+const NO_EFFECTS = 'no effects';
+
+describe('applicationStateMachine', () => {
+ const noEffectsToEmptyObject = effects => (typeof effects === 'string' ? {} : effects);
+
+ describe(`current state is ${NO_STATUS}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
+ ${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ ${UPDATING} | ${UPDATING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UPDATED} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: NO_STATUS,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${NOT_INSTALLABLE}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: NOT_INSTALLABLE,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLABLE}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLABLE,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLED}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLED,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+
+ describe(`current state is ${UPDATING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true, updateAcknowledged: false }}
+ ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: UPDATING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index b4d1bb710e0..1e896af1c7d 100644
--- a/spec/frontend/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
@@ -113,7 +113,6 @@ const DEFAULT_APPLICATION_STATE = {
description: 'Some description about this interesting application!',
status: null,
statusReason: null,
- requestStatus: null,
requestReason: null,
};
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index c0e8b737ea2..a20e0439555 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -32,15 +32,6 @@ describe('Clusters Store', () => {
});
describe('updateAppProperty', () => {
- it('should store new request status', () => {
- expect(store.state.applications.helm.requestStatus).toEqual(null);
-
- const newStatus = APPLICATION_STATUS.INSTALLING;
- store.updateAppProperty('helm', 'requestStatus', newStatus);
-
- expect(store.state.applications.helm.requestStatus).toEqual(newStatus);
- });
-
it('should store new request reason', () => {
expect(store.state.applications.helm.requestReason).toEqual(null);
@@ -68,80 +59,90 @@ describe('Clusters Store', () => {
title: 'Helm Tiller',
status: mockResponseData.applications[0].status,
statusReason: mockResponseData.applications[0].status_reason,
- requestStatus: null,
requestReason: null,
installed: false,
+ installFailed: false,
+ uninstallable: false,
},
ingress: {
title: 'Ingress',
- status: mockResponseData.applications[1].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[1].status_reason,
- requestStatus: null,
requestReason: null,
externalIp: null,
externalHostname: null,
installed: false,
+ installFailed: true,
+ uninstallable: false,
},
runner: {
title: 'GitLab Runner',
status: mockResponseData.applications[2].status,
statusReason: mockResponseData.applications[2].status_reason,
- requestStatus: null,
requestReason: null,
version: mockResponseData.applications[2].version,
upgradeAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
installed: false,
+ installFailed: false,
+ updateAcknowledged: true,
+ updateFailed: false,
+ updateSuccessful: false,
+ uninstallable: false,
},
prometheus: {
title: 'Prometheus',
- status: mockResponseData.applications[3].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[3].status_reason,
- requestStatus: null,
requestReason: null,
installed: false,
+ installFailed: true,
+ uninstallable: false,
},
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
statusReason: mockResponseData.applications[4].status_reason,
- requestStatus: null,
requestReason: null,
hostname: '',
installed: false,
+ installFailed: false,
+ uninstallable: false,
},
knative: {
title: 'Knative',
status: mockResponseData.applications[5].status,
statusReason: mockResponseData.applications[5].status_reason,
- requestStatus: null,
requestReason: null,
hostname: null,
isEditingHostName: false,
externalIp: null,
externalHostname: null,
installed: false,
+ installFailed: false,
+ uninstallable: false,
},
cert_manager: {
title: 'Cert-Manager',
- status: mockResponseData.applications[6].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
statusReason: mockResponseData.applications[6].status_reason,
- requestStatus: null,
requestReason: null,
email: mockResponseData.applications[6].email,
installed: false,
+ uninstallable: false,
},
},
});
});
- describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', () => {
+ describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', status => {
it('marks application as installed', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
const runnerAppIndex = 2;
- mockResponseData.applications[runnerAppIndex].status = APPLICATION_STATUS.INSTALLED;
+ mockResponseData.applications[runnerAppIndex].status = status;
store.updateStateFromServer(mockResponseData);
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
new file mode 100644
index 00000000000..17a998d0174
--- /dev/null
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -0,0 +1,185 @@
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { state, actions, getters, mutations } from '~/import_projects/store';
+import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
+import STATUS_MAP from '~/import_projects/constants';
+
+describe('ImportProjectsTable', () => {
+ let vm;
+ const providerTitle = 'THE PROVIDER';
+ const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
+ const importedProject = {
+ id: 1,
+ fullPath: 'fullPath',
+ importStatus: 'started',
+ providerLink: 'providerLink',
+ importSource: 'importSource',
+ };
+
+ function initStore() {
+ const stubbedActions = Object.assign({}, actions, {
+ fetchJobs: jest.fn(),
+ fetchRepos: jest.fn(actions.requestRepos),
+ fetchImport: jest.fn(actions.requestImport),
+ });
+
+ const store = new Vuex.Store({
+ state: state(),
+ actions: stubbedActions,
+ mutations,
+ getters,
+ });
+
+ return store;
+ }
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+
+ const component = mount(importProjectsTable, {
+ localVue,
+ store,
+ propsData: {
+ providerTitle,
+ },
+ sync: false,
+ });
+
+ return component.vm;
+ }
+
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a loading icon whilst repos are loading', () =>
+ vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
+ }));
+
+ it('renders a table with imported projects and provider repos', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [importedProject],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).not.toBeNull();
+ expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
+ `From ${providerTitle}`,
+ );
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+ });
+ });
+
+ it('renders an empty state if there are no imported projects or provider repos', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [],
+ namespaces: [],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).toBeNull();
+ expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
+ });
+ });
+
+ it('shows loading spinner when bulk import button is clicked', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$el.querySelector('.js-import-all').click();
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-import-all .js-loading-button-icon')).not.toBeNull();
+ });
+ });
+
+ it('imports provider repos if bulk import button is clicked', () => {
+ mountComponent();
+
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$store.dispatch('receiveImportSuccess', { importedProject, repoId: providerRepo.id });
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
+ });
+ });
+
+ it('polls to update the status of imported projects', () => {
+ const updatedProjects = [
+ {
+ id: importedProject.id,
+ importStatus: 'finished',
+ },
+ ];
+
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [importedProject],
+ providerRepos: [],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ const statusObject = STATUS_MAP[importedProject.importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+
+ vm.$store.dispatch('receiveJobsSuccess', updatedProjects);
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
index 7dac7e9ccc1..f95acc1edd7 100644
--- a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js
+++ b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
@@ -1,5 +1,6 @@
-import Vue from 'vue';
+import Vuex from 'vuex';
import createStore from '~/import_projects/store';
+import { createLocalVue, mount } from '@vue/test-utils';
import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import STATUS_MAP from '~/import_projects/constants';
@@ -13,27 +14,33 @@ describe('ImportedProjectTableRow', () => {
importSource: 'importSource',
};
- function createComponent() {
- const ImportedProjectTableRow = Vue.extend(importedProjectTableRow);
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
- const store = createStore();
- return new ImportedProjectTableRow({
- store,
+ const component = mount(importedProjectTableRow, {
+ localVue,
+ store: createStore(),
propsData: {
project: {
...project,
},
},
- }).$mount();
+ sync: false,
+ });
+
+ return component.vm;
}
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
afterEach(() => {
vm.$destroy();
});
it('renders an imported project table row', () => {
- vm = createComponent();
-
const providerLink = vm.$el.querySelector('.js-provider-link');
const statusObject = STATUS_MAP[project.importStatus];
diff --git a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
index 4d2bacd2ad0..02c786d8d0b 100644
--- a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
@@ -1,14 +1,15 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import createStore from '~/import_projects/store';
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { state, actions, getters, mutations } from '~/import_projects/store';
import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import STATUS_MAP, { STATUSES } from '~/import_projects/constants';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
describe('ProviderRepoTableRow', () => {
- let store;
let vm;
+ const fetchImport = jest.fn((context, data) => actions.requestImport(context, data));
+ const importPath = '/import-path';
+ const defaultTargetNamespace = 'user';
+ const ciCdOnly = true;
const repo = {
id: 10,
sanitizedName: 'sanitizedName',
@@ -16,21 +17,42 @@ describe('ProviderRepoTableRow', () => {
providerLink: 'providerLink',
};
- function createComponent() {
- const ProviderRepoTableRow = Vue.extend(providerRepoTableRow);
+ function initStore() {
+ const stubbedActions = Object.assign({}, actions, {
+ fetchImport,
+ });
- return new ProviderRepoTableRow({
+ const store = new Vuex.Store({
+ state: state(),
+ actions: stubbedActions,
+ mutations,
+ getters,
+ });
+
+ return store;
+ }
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+ store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
+
+ const component = mount(providerRepoTableRow, {
+ localVue,
store,
propsData: {
- repo: {
- ...repo,
- },
+ repo,
},
- }).$mount();
+ sync: false,
+ });
+
+ return component.vm;
}
beforeEach(() => {
- store = createStore();
+ vm = mountComponent();
});
afterEach(() => {
@@ -38,8 +60,6 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a provider repo table row', () => {
- vm = createComponent();
-
const providerLink = vm.$el.querySelector('.js-provider-link');
const statusObject = STATUS_MAP[STATUSES.NONE];
@@ -55,8 +75,6 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a select2 namespace select', () => {
- vm = createComponent();
-
const dropdownTrigger = vm.$el.querySelector('.js-namespace-select');
expect(dropdownTrigger).not.toBeNull();
@@ -67,30 +85,20 @@ describe('ProviderRepoTableRow', () => {
expect(vm.$el.querySelector('.select2-drop')).not.toBeNull();
});
- it('imports repo when clicking import button', done => {
- const importPath = '/import-path';
- const defaultTargetNamespace = 'user';
- const ciCdOnly = true;
- const mock = new MockAdapter(axios);
-
- store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
- mock.onPost(importPath).replyOnce(200);
- spyOn(store, 'dispatch').and.returnValue(new Promise(() => {}));
-
- vm = createComponent();
-
+ it('imports repo when clicking import button', () => {
vm.$el.querySelector('.js-import-button').click();
- setTimeoutPromise()
- .then(() => {
- expect(store.dispatch).toHaveBeenCalledWith('fetchImport', {
- repo,
- newName: repo.sanitizedName,
- targetNamespace: defaultTargetNamespace,
- });
- })
- .then(() => mock.restore())
- .then(done)
- .catch(done.fail);
+ return vm.$nextTick().then(() => {
+ const { calls } = fetchImport.mock;
+
+ // Not using .toBeCalledWith because it expects
+ // an unmatchable and undefined 3rd argument.
+ expect(calls.length).toBe(1);
+ expect(calls[0][1]).toEqual({
+ repo,
+ newName: repo.sanitizedName,
+ targetNamespace: defaultTargetNamespace,
+ });
+ });
});
});
diff --git a/spec/javascripts/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index 77850ee3283..6a7b90788dd 100644
--- a/spec/javascripts/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -27,8 +27,8 @@ import {
stopJobsPolling,
} from '~/import_projects/store/actions';
import state from '~/import_projects/store/state';
-import testAction from 'spec/helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'helpers/test_constants';
describe('import_projects store actions', () => {
let localState;
diff --git a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index 9eac75fac96..d1de98f4a15 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -2,8 +2,8 @@ import Vue from 'vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockAssigneesList } from 'spec/boards/mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockAssigneesList } from '../../../../javascripts/boards/mock_data';
const createComponent = (assignees = mockAssigneesList, cssClass = '') => {
const Component = Vue.extend(IssueAssignees);
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
new file mode 100644
index 00000000000..2e93ec412b9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -0,0 +1,172 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+
+import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
+
+import { mockMilestone } from '../../../../javascripts/boards/mock_data';
+
+const createComponent = (milestone = mockMilestone) => {
+ const Component = Vue.extend(IssueMilestone);
+
+ return mount(Component, {
+ propsData: {
+ milestone,
+ },
+ sync: false,
+ });
+};
+
+describe('IssueMilestoneComponent', () => {
+ let wrapper;
+ let vm;
+
+ beforeEach(done => {
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('isMilestoneStarted', () => {
+ it('should return `false` when milestoneStart prop is not defined', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestoneStarted).toBe(false);
+ });
+
+ it('should return `true` when milestone start date is past current date', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestoneStarted).toBe(true);
+ });
+ });
+
+ describe('isMilestonePastDue', () => {
+ it('should return `false` when milestoneDue prop is not defined', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestonePastDue).toBe(false);
+ });
+
+ it('should return `true` when milestone due is past current date', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '1990-07-22',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestonePastDue).toBe(true);
+ });
+ });
+
+ describe('milestoneDatesAbsolute', () => {
+ it('returns string containing absolute milestone due date', () => {
+ expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
+ });
+
+ it('returns string containing absolute milestone start date when due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
+ });
+
+ it('returns empty string when both milestone start and due dates are not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
+ });
+ });
+
+ describe('milestoneDatesHuman', () => {
+ it('returns string containing milestone due date when date is yet to be due', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: `${new Date().getFullYear() + 10}-01-01`,
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
+ });
+
+ it('returns string containing milestone start date when date has already started and due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
+ });
+
+ it('returns string containing milestone start date when date is yet to start and due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: `${new Date().getFullYear() + 10}-01-01`,
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
+ });
+
+ it('returns empty string when milestone start and due dates are not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toBe('');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component root element with class `issue-milestone-details`', () => {
+ expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
+ });
+
+ it('renders milestone icon', () => {
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
+ });
+
+ it('renders milestone title', () => {
+ expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
+ });
+
+ it('renders milestone tooltip', () => {
+ expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
+ mockMilestone.title,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
index aa7d6ea2e34..4a8de5fc4f1 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
const IssueWarning = Vue.extend(issueWarning);
@@ -19,7 +19,9 @@ describe('Issue Warning Component', () => {
isLocked: true,
});
- expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/lock$/);
+ expect(
+ vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).toMatch(/lock$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
'This issue is locked. Only project members can comment.',
);
@@ -32,7 +34,9 @@ describe('Issue Warning Component', () => {
isConfidential: true,
});
- expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/eye-slash$/);
+ expect(
+ vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).toMatch(/eye-slash$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
'This is a confidential issue. Your comment will not be visible to the public.',
);
diff --git a/spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
index 42198e92eea..e43d5301a50 100644
--- a/spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -1,7 +1,11 @@
import Vue from 'vue';
+import { formatDate } from '~/lib/utils/datetime_utility';
import { mount, createLocalVue } from '@vue/test-utils';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
-import { defaultMilestone, defaultAssignees } from './related_issuable_mock_data';
+import {
+ defaultAssignees,
+ defaultMilestone,
+} from '../../../../javascripts/vue_shared/components/issue/related_issuable_mock_data';
describe('RelatedIssuableItem', () => {
let wrapper;
@@ -85,11 +89,11 @@ describe('RelatedIssuableItem', () => {
it('renders state title', () => {
const stateTitle = tokenState.attributes('data-original-title');
+ const formatedCreateDate = formatDate(props.createdAt);
expect(stateTitle).toContain('<span class="bold">Opened</span>');
- expect(stateTitle).toContain(
- '<span class="text-tertiary">Dec 1, 2018 12:00am GMT+0000</span>',
- );
+
+ expect(stateTitle).toContain(`<span class="text-tertiary">${formatedCreateDate}</span>`);
});
it('renders aria label', () => {
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index 45f131194ca..eafff7f681e 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import createStore from '~/notes/stores';
-import { userDataMock } from '../../../notes/mock_data';
+import { userDataMock } from '../../../../javascripts/notes/mock_data';
describe('issue placeholder system note component', () => {
let store;
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
index 6013e85811a..976e38c15ee 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('placeholder system note component', () => {
let PlaceholderSystemNote;
diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index adcb1c858aa..dc66150ab8d 100644
--- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
import issueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import createStore from '~/notes/stores';
+import initMRPopovers from '~/mr_popover/index';
+
+jest.mock('~/mr_popover/index', () => jest.fn());
describe('system note component', () => {
let vm;
@@ -56,4 +59,8 @@ describe('system note component', () => {
it('removes wrapping paragraph from note HTML', () => {
expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>');
});
+
+ it('should initMRPopovers onMount', () => {
+ expect(initMRPopovers).toHaveBeenCalled();
+ });
});
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 5f9c180cbb7..399a33dae75 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -4,104 +4,119 @@ describe Resolvers::IssuesResolver do
include GraphqlHelpers
let(:current_user) { create(:user) }
- set(:project) { create(:project) }
- set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) }
- set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) }
- set(:label1) { create(:label, project: project) }
- set(:label2) { create(:label, project: project) }
-
- before do
- project.add_developer(current_user)
- create(:label_link, label: label1, target: issue1)
- create(:label_link, label: label1, target: issue2)
- create(:label_link, label: label2, target: issue2)
- end
-
- describe '#resolve' do
- it 'finds all issues' do
- expect(resolve_issues).to contain_exactly(issue1, issue2)
- end
- it 'filters by state' do
- expect(resolve_issues(state: 'opened')).to contain_exactly(issue1)
- expect(resolve_issues(state: 'closed')).to contain_exactly(issue2)
+ context "with a project" do
+ set(:project) { create(:project) }
+ set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) }
+ set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) }
+ set(:label1) { create(:label, project: project) }
+ set(:label2) { create(:label, project: project) }
+
+ before do
+ project.add_developer(current_user)
+ create(:label_link, label: label1, target: issue1)
+ create(:label_link, label: label1, target: issue2)
+ create(:label_link, label: label2, target: issue2)
end
- it 'filters by labels' do
- expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
- expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
- end
+ describe '#resolve' do
+ it 'finds all issues' do
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
+ end
- describe 'filters by created_at' do
- it 'filters by created_before' do
- expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
+ it 'filters by state' do
+ expect(resolve_issues(state: 'opened')).to contain_exactly(issue1)
+ expect(resolve_issues(state: 'closed')).to contain_exactly(issue2)
end
- it 'filters by created_after' do
- expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2)
+ it 'filters by labels' do
+ expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
+ expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
end
- end
- describe 'filters by updated_at' do
- it 'filters by updated_before' do
- expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1)
+ describe 'filters by created_at' do
+ it 'filters by created_before' do
+ expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
+ end
+
+ it 'filters by created_after' do
+ expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- it 'filters by updated_after' do
- expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2)
+ describe 'filters by updated_at' do
+ it 'filters by updated_before' do
+ expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1)
+ end
+
+ it 'filters by updated_after' do
+ expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- end
- describe 'filters by closed_at' do
- let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) }
+ describe 'filters by closed_at' do
+ let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) }
- it 'filters by closed_before' do
- expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3)
+ it 'filters by closed_before' do
+ expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3)
+ end
+
+ it 'filters by closed_after' do
+ expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- it 'filters by closed_after' do
- expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2)
+ it 'searches issues' do
+ expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
end
- end
- it 'searches issues' do
- expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
- end
+ it 'sort issues' do
+ expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
+ end
- it 'sort issues' do
- expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
- end
+ it 'returns issues user can see' do
+ project.add_guest(current_user)
- it 'returns issues user can see' do
- project.add_guest(current_user)
+ create(:issue, confidential: true)
- create(:issue, confidential: true)
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
+ end
- expect(resolve_issues).to contain_exactly(issue1, issue2)
- end
+ it 'finds a specific issue with iid' do
+ expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1)
+ end
- it 'finds a specific issue with iid' do
- expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1)
- end
+ it 'finds a specific issue with iids' do
+ expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1)
+ end
- it 'finds a specific issue with iids' do
- expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1)
- end
+ it 'finds multiple issues with iids' do
+ expect(resolve_issues(iids: [issue1.iid, issue2.iid]))
+ .to contain_exactly(issue1, issue2)
+ end
- it 'finds multiple issues with iids' do
- expect(resolve_issues(iids: [issue1.iid, issue2.iid]))
- .to contain_exactly(issue1, issue2)
- end
+ it 'finds only the issues within the project we are looking at' do
+ another_project = create(:project)
+ iids = [issue1, issue2].map(&:iid)
+
+ iids.each do |iid|
+ create(:issue, project: another_project, iid: iid)
+ end
- it 'finds only the issues within the project we are looking at' do
- another_project = create(:project)
- iids = [issue1, issue2].map(&:iid)
+ expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2)
+ end
+ end
+ end
- iids.each do |iid|
- create(:issue, project: another_project, iid: iid)
+ context "when passing a non existent, batch loaded project" do
+ let(:project) do
+ BatchLoader.for("non-existent-path").batch do |_fake_paths, loader, _|
+ loader.call("non-existent-path", nil)
end
+ end
- expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2)
+ it "returns nil without breaking" do
+ expect(resolve_issues(iids: ["don't", "break"])).to be_empty
end
end
diff --git a/spec/javascripts/fixtures/images/green_box.png b/spec/javascripts/fixtures/static/images/green_box.png
index cd1ff9f9ade..cd1ff9f9ade 100644
--- a/spec/javascripts/fixtures/images/green_box.png
+++ b/spec/javascripts/fixtures/static/images/green_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/one_white_pixel.png b/spec/javascripts/fixtures/static/images/one_white_pixel.png
index 073fcf40a18..073fcf40a18 100644
--- a/spec/javascripts/fixtures/one_white_pixel.png
+++ b/spec/javascripts/fixtures/static/images/one_white_pixel.png
Binary files differ
diff --git a/spec/javascripts/fixtures/images/red_box.png b/spec/javascripts/fixtures/static/images/red_box.png
index 73b2927da0f..73b2927da0f 100644
--- a/spec/javascripts/fixtures/images/red_box.png
+++ b/spec/javascripts/fixtures/static/images/red_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/static/projects.json
index 68a150f602a..68a150f602a 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/static/projects.json
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 57e31d933ca..8c7820ddb52 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -6,7 +6,7 @@ import '~/lib/utils/common_utils';
describe('glDropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html');
- loadJSONFixtures('projects.json');
+ loadJSONFixtures('static/projects.json');
const NON_SELECTABLE_CLASSES =
'.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
@@ -67,7 +67,7 @@ describe('glDropdown', function describeDropdown() {
loadFixtures('static/gl_dropdown.html');
this.dropdownContainerElement = $('.dropdown.inline');
this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = getJSONFixture('projects.json');
+ this.projectsData = getJSONFixture('static/projects.json');
});
afterEach(() => {
diff --git a/spec/javascripts/import_projects/components/import_projects_table_spec.js b/spec/javascripts/import_projects/components/import_projects_table_spec.js
deleted file mode 100644
index ab8642bf0dd..00000000000
--- a/spec/javascripts/import_projects/components/import_projects_table_spec.js
+++ /dev/null
@@ -1,188 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import createStore from '~/import_projects/store';
-import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
-import STATUS_MAP from '~/import_projects/constants';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
-
-describe('ImportProjectsTable', () => {
- let vm;
- let mock;
- let store;
- const reposPath = '/repos-path';
- const jobsPath = '/jobs-path';
- const providerTitle = 'THE PROVIDER';
- const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
- const importedProject = {
- id: 1,
- fullPath: 'fullPath',
- importStatus: 'started',
- providerLink: 'providerLink',
- importSource: 'importSource',
- };
-
- function createComponent() {
- const ImportProjectsTable = Vue.extend(importProjectsTable);
-
- const component = new ImportProjectsTable({
- store,
- propsData: {
- providerTitle,
- },
- }).$mount();
-
- store.dispatch('stopJobsPolling');
-
- return component;
- }
-
- beforeEach(() => {
- store = createStore();
- store.dispatch('setInitialData', { reposPath });
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- vm.$destroy();
- mock.restore();
- });
-
- it('renders a loading icon whilst repos are loading', done => {
- mock.restore(); // Stop the mock adapter from responding to the request, keeping the spinner up
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('renders a table with imported projects and provider repos', done => {
- const response = {
- importedProjects: [importedProject],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- };
- mock.onGet(reposPath).reply(200, response);
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).not.toBeNull();
- expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
- `From ${providerTitle}`,
- );
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('renders an empty state if there are no imported projects or provider repos', done => {
- const response = {
- importedProjects: [],
- providerRepos: [],
- namespaces: [],
- };
- mock.onGet(reposPath).reply(200, response);
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).toBeNull();
- expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('imports provider repos if bulk import button is clicked', done => {
- const importPath = '/import-path';
- const response = {
- importedProjects: [],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- };
-
- mock.onGet(reposPath).replyOnce(200, response);
- mock.onPost(importPath).replyOnce(200, importedProject);
-
- store.dispatch('setInitialData', { importPath });
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
-
- vm.$el.querySelector('.js-import-all').click();
- })
- .then(() => setTimeoutPromise())
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('polls to update the status of imported projects', done => {
- const importPath = '/import-path';
- const response = {
- importedProjects: [importedProject],
- providerRepos: [],
- namespaces: [{ path: 'path' }],
- };
- const updatedProjects = [
- {
- id: importedProject.id,
- importStatus: 'finished',
- },
- ];
-
- mock.onGet(reposPath).replyOnce(200, response);
-
- store.dispatch('setInitialData', { importPath, jobsPath });
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- const statusObject = STATUS_MAP[importedProject.importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
-
- mock.onGet(jobsPath).replyOnce(200, updatedProjects);
- return store.dispatch('restartJobsPolling');
- })
- .then(() => setTimeoutPromise())
- .then(() => {
- const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-});
diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js
index a820dd2d09c..24b5512b053 100644
--- a/spec/javascripts/test_constants.js
+++ b/spec/javascripts/test_constants.js
@@ -1,7 +1,7 @@
export const FIXTURES_PATH = '/base/spec/javascripts/fixtures';
export const TEST_HOST = 'http://test.host';
-export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/one_white_pixel.png`;
+export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`;
-export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/green_box.png`;
-export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/red_box.png`;
+export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/green_box.png`;
+export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/red_box.png`;
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 690fcd3e224..a0628fdcebe 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -228,6 +228,7 @@ describe('mrWidgetOptions', () => {
describe('showTargetBranchAdvancedError', () => {
describe(`when the pipeline's target_sha property doesn't exist`, () => {
beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
Vue.set(vm.mr.pipeline, 'target_sha', undefined);
Vue.set(vm.mr, 'targetBranchSha', 'abcd');
vm.$nextTick(done);
@@ -240,6 +241,7 @@ describe('mrWidgetOptions', () => {
describe(`when the pipeline's target_sha matches the target branch's sha`, () => {
beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
Vue.set(vm.mr, 'targetBranchSha', 'abcd');
vm.$nextTick(done);
@@ -250,8 +252,22 @@ describe('mrWidgetOptions', () => {
});
});
+ describe(`when the merge request is not open`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', false);
+ Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
+ Vue.set(vm.mr, 'targetBranchSha', 'bcde');
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(false);
+ });
+ });
+
describe(`when the pipeline's target_sha does not match the target branch's sha`, () => {
beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
Vue.set(vm.mr, 'targetBranchSha', 'bcde');
vm.$nextTick(done);
diff --git a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js b/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
deleted file mode 100644
index 8fca2637326..00000000000
--- a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import Vue from 'vue';
-
-import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockMilestone } from 'spec/boards/mock_data';
-
-const createComponent = (milestone = mockMilestone) => {
- const Component = Vue.extend(IssueMilestone);
-
- return mountComponent(Component, {
- milestone,
- });
-};
-
-describe('IssueMilestoneComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isMilestoneStarted', () => {
- it('should return `false` when milestoneStart prop is not defined', done => {
- const vmStartUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStartUndefined.isMilestoneStarted).toBe(false);
- })
- .then(done)
- .catch(done.fail);
-
- vmStartUndefined.$destroy();
- });
-
- it('should return `true` when milestone start date is past current date', done => {
- const vmStarted = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarted.isMilestoneStarted).toBe(true);
- })
- .then(done)
- .catch(done.fail);
-
- vmStarted.$destroy();
- });
- });
-
- describe('isMilestonePastDue', () => {
- it('should return `false` when milestoneDue prop is not defined', done => {
- const vmDueUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDueUndefined.isMilestonePastDue).toBe(false);
- })
- .then(done)
- .catch(done.fail);
-
- vmDueUndefined.$destroy();
- });
-
- it('should return `true` when milestone due is past current date', done => {
- const vmPastDue = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '1990-07-22',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmPastDue.isMilestonePastDue).toBe(true);
- })
- .then(done)
- .catch(done.fail);
-
- vmPastDue.$destroy();
- });
- });
-
- describe('milestoneDatesAbsolute', () => {
- it('returns string containing absolute milestone due date', () => {
- expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
- });
-
- it('returns string containing absolute milestone start date when due date is not present', done => {
- const vmDueUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDueUndefined.milestoneDatesAbsolute).toBe('(January 1, 2018)');
- })
- .then(done)
- .catch(done.fail);
-
- vmDueUndefined.$destroy();
- });
-
- it('returns empty string when both milestone start and due dates are not present', done => {
- const vmDatesUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDatesUndefined.milestoneDatesAbsolute).toBe('');
- })
- .then(done)
- .catch(done.fail);
-
- vmDatesUndefined.$destroy();
- });
- });
-
- describe('milestoneDatesHuman', () => {
- it('returns string containing milestone due date when date is yet to be due', done => {
- const vmFuture = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: `${new Date().getFullYear() + 10}-01-01`,
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmFuture.milestoneDatesHuman).toContain('years remaining');
- })
- .then(done)
- .catch(done.fail);
-
- vmFuture.$destroy();
- });
-
- it('returns string containing milestone start date when date has already started and due date is not present', done => {
- const vmStarted = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarted.milestoneDatesHuman).toContain('Started');
- })
- .then(done)
- .catch(done.fail);
-
- vmStarted.$destroy();
- });
-
- it('returns string containing milestone start date when date is yet to start and due date is not present', done => {
- const vmStarts = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: `${new Date().getFullYear() + 10}-01-01`,
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarts.milestoneDatesHuman).toContain('Starts');
- })
- .then(done)
- .catch(done.fail);
-
- vmStarts.$destroy();
- });
-
- it('returns empty string when milestone start and due dates are not present', done => {
- const vmDatesUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDatesUndefined.milestoneDatesHuman).toBe('');
- })
- .then(done)
- .catch(done.fail);
-
- vmDatesUndefined.$destroy();
- });
- });
- });
-
- describe('template', () => {
- it('renders component root element with class `issue-milestone-details`', () => {
- expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
- });
-
- it('renders milestone icon', () => {
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
- });
-
- it('renders milestone title', () => {
- expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
- });
-
- it('renders milestone tooltip', () => {
- expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
- mockMilestone.title,
- );
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
index b95183747bb..268ced38f40 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
@@ -9,8 +9,8 @@ describe('ProjectListItem component', () => {
let wrapper;
let vm;
let options;
- loadJSONFixtures('projects.json');
- const project = getJSONFixture('projects.json')[0];
+ loadJSONFixtures('static/projects.json');
+ const project = getJSONFixture('static/projects.json')[0];
beforeEach(() => {
options = {
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
index ba9ec8f2f19..34c0cd435cd 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
@@ -8,8 +8,8 @@ import { trimText } from 'spec/helpers/vue_component_helper';
describe('ProjectSelector component', () => {
let wrapper;
let vm;
- loadJSONFixtures('projects.json');
- const allProjects = getJSONFixture('projects.json');
+ loadJSONFixtures('static/projects.json');
+ const allProjects = getJSONFixture('static/projects.json');
const searchResults = allProjects.slice(0, 5);
let selected = [];
selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index 3ff2fe18c15..613814df23f 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -137,19 +137,5 @@ describe Gitlab::Ci::Variables::Collection::Item do
.to eq(key: 'VAR', value: 'value', public: true, file: true, masked: false)
end
end
-
- context 'when variable masking is disabled' do
- before do
- stub_feature_flags(variable_masking: false)
- end
-
- it 'does not expose the masked field to the runner' do
- runner_variable = described_class
- .new(key: 'VAR', value: 'value', masked: true)
- .to_runner_variable
-
- expect(runner_variable).to eq(key: 'VAR', value: 'value', public: true)
- end
- end
end
end
diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb
new file mode 100644
index 00000000000..b89a44e178b
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/deployment_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Deployment do
+ describe '.build' do
+ it 'returns the object kind for a deployment' do
+ deployment = build(:deployment)
+
+ data = described_class.build(deployment)
+
+ expect(data[:object_kind]).to eq('deployment')
+ end
+
+ it 'returns data for the given build' do
+ environment = create(:environment, name: "somewhere")
+ project = create(:project, :repository, name: 'myproj')
+ commit = project.commit('HEAD')
+ deployment = create(:deployment, status: :failed, environment: environment, sha: commit.sha, project: project)
+ deployable = deployment.deployable
+ expected_deployable_url = Gitlab::Routing.url_helpers.project_job_url(deployable.project, deployable)
+ expected_commit_url = Gitlab::UrlBuilder.build(commit)
+
+ data = described_class.build(deployment)
+
+ expect(data[:status]).to eq('failed')
+ expect(data[:deployable_id]).to eq(deployable.id)
+ expect(data[:deployable_url]).to eq(expected_deployable_url)
+ expect(data[:environment]).to eq("somewhere")
+ expect(data[:project]).to eq(project.hook_attrs)
+ expect(data[:short_sha]).to eq(deployment.short_sha)
+ expect(data[:user]).to eq(deployment.user.hook_attrs)
+ expect(data[:commit_url]).to eq(expected_commit_url)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 45fe5d72937..5f8a2848944 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -95,6 +95,12 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#create_repository' do
+ it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :create_repository do
+ subject { repository.create_repository }
+ end
+ end
+
describe '#branch_names' do
subject { repository.branch_names }
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index f1acb1d9bc4..da1eb0c2618 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -142,6 +142,48 @@ describe Gitlab::GitalyClient do
end
end
+ describe '.request_kwargs' do
+ context 'when catfile-cache feature is enabled' do
+ before do
+ stub_feature_flags('gitaly_catfile-cache': true)
+ end
+
+ it 'sets the gitaly-session-id in the metadata' do
+ results = described_class.request_kwargs('default', nil)
+ expect(results[:metadata]).to include('gitaly-session-id')
+ end
+
+ context 'when RequestStore is not enabled' do
+ it 'sets a different gitaly-session-id per request' do
+ gitaly_session_id = described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']
+
+ expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id)
+ end
+ end
+
+ context 'when RequestStore is enabled', :request_store do
+ it 'sets the same gitaly-session-id on every outgoing request metadata' do
+ gitaly_session_id = described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']
+
+ 3.times do
+ expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id)
+ end
+ end
+ end
+ end
+
+ context 'when catfile-cache feature is disabled' do
+ before do
+ stub_feature_flags({ 'gitaly_catfile-cache': false })
+ end
+
+ it 'does not set the gitaly-session-id in the metadata' do
+ results = described_class.request_kwargs('default', nil)
+ expect(results[:metadata]).not_to include('gitaly-session-id')
+ end
+ end
+ end
+
describe 'enforce_gitaly_request_limits?' do
def call_gitaly(count = 1)
(1..count).each do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 54369ff75f4..482e9c05da8 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -322,6 +322,7 @@ project:
- pool_repository
- kubernetes_namespaces
- error_tracking_setting
+- metrics_setting
award_emoji:
- awardable
- user
@@ -360,3 +361,5 @@ error_tracking_setting:
- project
suggestions:
- note
+metrics_setting:
+- project
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index ebb62124cb1..9093d21647a 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -423,6 +423,7 @@ Service:
- wiki_page_events
- confidential_issues_events
- confidential_note_events
+- deployment_events
ProjectHook:
- id
- url
@@ -606,7 +607,6 @@ ResourceLabelEvent:
- user_id
- created_at
ErrorTracking::ProjectErrorTrackingSetting:
-- id
- api_url
- project_id
- project_name
@@ -626,3 +626,8 @@ MergeRequestAssignee:
- id
- user_id
- merge_request_id
+ProjectMetricsSetting:
+- project_id
+- external_dashboard_url
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
new file mode 100644
index 00000000000..ee7c93fce8d
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Processor do
+ let(:project) { build(:project) }
+ let(:environment) { build(:environment) }
+ let(:dashboard_yml) { YAML.load_file('spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+
+ describe 'process' do
+ let(:process_params) { [project, environment, dashboard_yml] }
+ let(:dashboard) { described_class.new(*process_params).process }
+
+ context 'when dashboard config corresponds to common metrics' do
+ let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
+
+ it 'inserts metric ids into the config' do
+ target_metric = all_metrics.find { |metric| metric[:id] == 'metric_a1' }
+
+ expect(target_metric).to include(:metric_id)
+ expect(target_metric[:metric_id]).to eq(common_metric.id)
+ end
+ end
+
+ context 'when the project has associated metrics' do
+ let!(:project_response_metric) { create(:prometheus_metric, project: project, group: :response) }
+ let!(:project_system_metric) { create(:prometheus_metric, project: project, group: :system) }
+ let!(:project_business_metric) { create(:prometheus_metric, project: project, group: :business) }
+
+ it 'includes project-specific metrics' do
+ expect(all_metrics).to include get_metric_details(project_system_metric)
+ expect(all_metrics).to include get_metric_details(project_response_metric)
+ expect(all_metrics).to include get_metric_details(project_business_metric)
+ end
+
+ it 'orders groups by priority and panels by weight' do
+ expected_metrics_order = [
+ 'metric_a2', # group priority 10, panel weight 2
+ 'metric_a1', # group priority 10, panel weight 1
+ 'metric_b', # group priority 1, panel weight 1
+ project_business_metric.id, # group priority 0, panel weight nil (0)
+ project_response_metric.id, # group priority -5, panel weight nil (0)
+ project_system_metric.id, # group priority -10, panel weight nil (0)
+ ]
+ actual_metrics_order = all_metrics.map { |m| m[:id] || m[:metric_id] }
+
+ expect(actual_metrics_order).to eq expected_metrics_order
+ end
+ end
+
+ shared_examples_for 'errors with message' do |expected_message|
+ it 'raises a DashboardLayoutError' do
+ error_class = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError
+
+ expect { dashboard }.to raise_error(error_class, expected_message)
+ end
+ end
+
+ context 'when the dashboard is missing panel_groups' do
+ let(:dashboard_yml) { {} }
+
+ it_behaves_like 'errors with message', 'Top-level key :panel_groups must be an array'
+ end
+
+ context 'when the dashboard contains a panel_group which is missing panels' do
+ let(:dashboard_yml) { { panel_groups: [{}] } }
+
+ it_behaves_like 'errors with message', 'Each "panel_group" must define an array :panels'
+ end
+
+ context 'when the dashboard contains a panel which is missing metrics' do
+ let(:dashboard_yml) { { panel_groups: [{ panels: [{}] }] } }
+
+ it_behaves_like 'errors with message', 'Each "panel" must define an array :metrics'
+ end
+ end
+
+ private
+
+ def all_metrics
+ dashboard[:panel_groups].map do |group|
+ group[:panels].map { |panel| panel[:metrics] }
+ end.flatten
+ end
+
+ def get_metric_details(metric)
+ {
+ query_range: metric.query,
+ unit: metric.unit,
+ label: metric.legend,
+ metric_id: metric.id
+ }
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/service_spec.rb b/spec/lib/gitlab/metrics/dashboard/service_spec.rb
new file mode 100644
index 00000000000..e66c356bf49
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/service_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Service, :use_clean_rails_memory_store_caching do
+ let(:project) { build(:project) }
+ let(:environment) { build(:environment) }
+
+ describe 'get_dashboard' do
+ let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
+
+ it 'returns a json representation of the environment dashboard' do
+ result = described_class.new(project, environment).get_dashboard
+
+ expect(result.keys).to contain_exactly(:dashboard, :status)
+ expect(result[:status]).to eq(:success)
+
+ expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty
+ end
+
+ it 'caches the dashboard for subsequent calls' do
+ expect(YAML).to receive(:safe_load).once.and_call_original
+
+ described_class.new(project, environment).get_dashboard
+ described_class.new(project, environment).get_dashboard
+ end
+
+ context 'when the dashboard is configured incorrectly' do
+ before do
+ allow(YAML).to receive(:safe_load).and_return({})
+ end
+
+ it 'returns an appropriate message and status code' do
+ result = described_class.new(project, environment).get_dashboard
+
+ expect(result.keys).to contain_exactly(:message, :http_status, :status)
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(:unprocessable_entity)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 9f2214f7ce7..5af52db7a1f 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -27,13 +27,13 @@ describe Gitlab::Profiler do
it 'sends a POST request when data is passed' do
post_data = '{"a":1}'
- expect(app).to receive(:post).with(anything, post_data, anything)
+ expect(app).to receive(:post).with(anything, params: post_data, headers: anything)
described_class.profile('/', post_data: post_data)
end
it 'uses the private_token for auth if given' do
- expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token)
+ expect(app).to receive(:get).with('/', params: nil, headers: { 'Private-Token' => private_token })
expect(app).to receive(:get).with('/api/v4/users')
described_class.profile('/', private_token: private_token)
@@ -51,7 +51,7 @@ describe Gitlab::Profiler do
user = double(:user)
expect(described_class).to receive(:with_user).with(nil).and_call_original
- expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token)
+ expect(app).to receive(:get).with('/', params: nil, headers: { 'Private-Token' => private_token })
expect(app).to receive(:get).with('/api/v4/users')
described_class.profile('/', user: user, private_token: private_token)
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index c7d7dbac736..f8dc1541dd3 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -31,6 +31,20 @@ describe ApplicationSetting do
it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) }
it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) }
+ it { is_expected.to allow_value("myemail@gitlab.com").for(:lets_encrypt_notification_email) }
+ it { is_expected.to allow_value(nil).for(:lets_encrypt_notification_email) }
+ it { is_expected.not_to allow_value("notanemail").for(:lets_encrypt_notification_email) }
+ it { is_expected.not_to allow_value("myemail@example.com").for(:lets_encrypt_notification_email) }
+ it { is_expected.to allow_value("myemail@test.example.com").for(:lets_encrypt_notification_email) }
+
+ context "when user accepted let's encrypt terms of service" do
+ before do
+ setting.update(lets_encrypt_terms_of_service_accepted: true)
+ end
+
+ it { is_expected.not_to allow_value(nil).for(:lets_encrypt_notification_email) }
+ end
+
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 3a7d20a58c8..339483d4f7d 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -856,6 +856,10 @@ describe Ci::Build do
let(:deployment) { build.deployment }
let(:environment) { deployment.environment }
+ before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
+ end
+
it 'has deployments record with created status' do
expect(deployment).to be_created
expect(environment.name).to eq('review/master')
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index de406211a5b..b66acf13135 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -24,7 +24,7 @@ describe Clusters::Applications::Runner do
it 'is initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
- expect(subject.version).to eq('0.4.0')
+ expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
expect(subject).to be_rbac
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.files).to eq(gitlab_runner.files)
@@ -42,7 +42,7 @@ describe Clusters::Applications::Runner do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('0.4.0')
+ expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
end
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index d9170d5fa07..f51322e1404 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -102,6 +102,13 @@ describe Deployment do
deployment.succeed!
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.succeed!
+ end
end
context 'when deployment failed' do
@@ -115,6 +122,13 @@ describe Deployment do
expect(deployment.finished_at).to be_like_time(Time.now)
end
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.drop!
+ end
end
context 'when deployment was canceled' do
@@ -128,6 +142,13 @@ describe Deployment do
expect(deployment.finished_at).to be_like_time(Time.now)
end
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.cancel!
+ end
end
end
@@ -379,6 +400,12 @@ describe Deployment do
it { is_expected.to be_nil }
end
+ context 'project uses the kubernetes service for deployments' do
+ let!(:service) { create(:kubernetes_service, project: project) }
+
+ it { is_expected.to be_nil }
+ end
+
context 'project has a deployment platform' do
let!(:cluster) { create(:cluster, projects: [project]) }
let!(:platform) { create(:cluster_platform_kubernetes, cluster: cluster) }
diff --git a/spec/models/project_metrics_setting_spec.rb b/spec/models/project_metrics_setting_spec.rb
new file mode 100644
index 00000000000..7df01625ba1
--- /dev/null
+++ b/spec/models/project_metrics_setting_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectMetricsSetting do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'Validations' do
+ context 'when external_dashboard_url is over 255 chars' do
+ before do
+ subject.external_dashboard_url = 'https://' + 'a' * 250
+ end
+
+ it 'fails validation' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages[:external_dashboard_url])
+ .to include('is too long (maximum is 255 characters)')
+ end
+ end
+
+ context 'with unsafe url' do
+ before do
+ subject.external_dashboard_url = %{https://replaceme.com/'><script>alert(document.cookie)</script>}
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'non ascii chars in external_dashboard_url' do
+ before do
+ subject.external_dashboard_url = 'http://gitlab.com/api/0/projects/project1/something€'
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'internal url in external_dashboard_url' do
+ before do
+ subject.external_dashboard_url = 'http://192.168.1.1'
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'external_dashboard_url is blank' do
+ before do
+ subject.external_dashboard_url = ''
+ end
+
+ it { is_expected.to be_invalid }
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_message/deployment_message_spec.rb b/spec/models/project_services/chat_message/deployment_message_spec.rb
new file mode 100644
index 00000000000..86565ce8b01
--- /dev/null
+++ b/spec/models/project_services/chat_message/deployment_message_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ChatMessage::DeploymentMessage do
+ describe '#pretext' do
+ it 'returns a message with the data returned by the deployment data builder' do
+ environment = create(:environment, name: "myenvironment")
+ project = create(:project, :repository)
+ commit = project.commit('HEAD')
+ deployment = create(:deployment, status: :success, environment: environment, project: project, sha: commit.sha)
+ data = Gitlab::DataBuilder::Deployment.build(deployment)
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq("Deploy to myenvironment succeeded")
+ end
+
+ it 'returns a message for a successful deployment' do
+ data = {
+ status: 'success',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production succeeded')
+ end
+
+ it 'returns a message for a failed deployment' do
+ data = {
+ status: 'failed',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production failed')
+ end
+
+ it 'returns a message for a canceled deployment' do
+ data = {
+ status: 'canceled',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production canceled')
+ end
+
+ it 'returns a message for a deployment to another environment' do
+ data = {
+ status: 'success',
+ environment: 'staging'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to staging succeeded')
+ end
+
+ it 'returns a message for a deployment with any other status' do
+ data = {
+ status: 'unknown',
+ environment: 'staging'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to staging unknown')
+ end
+ end
+
+ describe '#attachments' do
+ def deployment_data(params)
+ {
+ object_kind: "deployment",
+ status: "success",
+ deployable_id: 3,
+ deployable_url: "deployable_url",
+ environment: "sandbox",
+ project: {
+ name: "greatproject",
+ web_url: "project_web_url",
+ path_with_namespace: "project_path_with_namespace"
+ },
+ user: {
+ name: "Jane Person",
+ username: "jane"
+ },
+ short_sha: "12345678",
+ commit_url: "commit_url"
+ }.merge(params)
+ end
+
+ it 'returns attachments with the data returned by the deployment data builder' do
+ user = create(:user, name: "John Smith", username: "smith")
+ namespace = create(:namespace, name: "myspace")
+ project = create(:project, :repository, namespace: namespace, name: "myproject")
+ commit = project.commit('HEAD')
+ environment = create(:environment, name: "myenvironment", project: project)
+ ci_build = create(:ci_build, project: project)
+ deployment = create(:deployment, :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha)
+ job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build)
+ commit_url = Gitlab::UrlBuilder.build(deployment.commit)
+ data = Gitlab::DataBuilder::Deployment.build(deployment)
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[myspace/myproject](#{project.web_url})\n[Job ##{ci_build.id}](#{job_url}), SHA [#{deployment.short_sha}](#{commit_url}), by John Smith (smith)",
+ color: "good"
+ }])
+ end
+
+ it 'returns attachments for a failed deployment' do
+ data = deployment_data(status: 'failed')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)",
+ color: "danger"
+ }])
+ end
+
+ it 'returns attachments for a canceled deployment' do
+ data = deployment_data(status: 'canceled')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)",
+ color: "warning"
+ }])
+ end
+
+ it 'uses a neutral color for a deployment with any other status' do
+ data = deployment_data(status: 'some-new-status-we-make-in-the-future')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url)\n[Job #3](deployable_url), SHA [12345678](commit_url), by Jane Person (jane)",
+ color: "#334455"
+ }])
+ end
+ end
+end
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index 521d5265753..c025d7c882e 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -30,6 +30,12 @@ describe MicrosoftTeamsService do
end
end
+ describe '.supported_events' do
+ it 'does not support deployment_events' do
+ expect(described_class.supported_events).not_to include('deployment')
+ end
+ end
+
describe "#execute" do
let(:user) { create(:user) }
set(:project) { create(:project, :repository, :wiki_repo) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 4c354593b57..43ec1125087 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2487,4 +2487,69 @@ describe Repository do
repository.merge_base('master', 'fix')
end
end
+
+ describe '#create_if_not_exists' do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ it 'creates the repository if it did not exist' do
+ expect { repository.create_if_not_exists }.to change { repository.exists? }.from(false).to(true)
+ end
+
+ it 'calls out to the repository client to create a repo' do
+ expect(repository.raw.gitaly_repository_client).to receive(:create_repository)
+
+ repository.create_if_not_exists
+ end
+
+ context 'it does nothing if the repository already existed' do
+ let(:project) { create(:project, :repository) }
+
+ it 'does nothing if the repository already existed' do
+ expect(repository.raw.gitaly_repository_client).not_to receive(:create_repository)
+
+ repository.create_if_not_exists
+ end
+ end
+
+ context 'when the repository exists but the cache is not up to date' do
+ let(:project) { create(:project, :repository) }
+
+ it 'does not raise errors' do
+ allow(repository).to receive(:exists?).and_return(false)
+ expect(repository.raw).to receive(:create_repository).and_call_original
+
+ expect { repository.create_if_not_exists }.not_to raise_error
+ end
+ end
+ end
+
+ describe "#blobs_metadata" do
+ set(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+
+ def expect_metadata_blob(thing)
+ expect(thing).to be_a(Blob)
+ expect(thing.data).to be_empty
+ end
+
+ it "returns blob metadata in batch for HEAD" do
+ result = repository.blobs_metadata(["bar/branch-test.txt", "README.md", "does/not/exist"])
+
+ expect_metadata_blob(result.first)
+ expect_metadata_blob(result.second)
+ expect(result.size).to eq(2)
+ end
+
+ it "returns blob metadata for a specified ref" do
+ result = repository.blobs_metadata(["files/ruby/feature.rb"], "feature")
+
+ expect_metadata_blob(result.first)
+ end
+
+ it "performs a single gitaly call", :request_store do
+ expect { repository.blobs_metadata(["bar/branch-test.txt", "readme.txt", "does/not/exist"]) }
+ .to change { Gitlab::GitalyClient.get_request_count }.by(1)
+ end
+ end
end
diff --git a/spec/rubocop/cop/include_action_view_context_spec.rb b/spec/rubocop/cop/include_action_view_context_spec.rb
new file mode 100644
index 00000000000..c888555b54f
--- /dev/null
+++ b/spec/rubocop/cop/include_action_view_context_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../rubocop/cop/include_action_view_context'
+
+describe RuboCop::Cop::IncludeActionViewContext do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when `ActionView::Context` is included' do
+ let(:source) { 'include ActionView::Context' }
+ let(:correct_source) { 'include ::Gitlab::ActionViewOutput::Context' }
+
+ it 'registers an offense' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['ActionView::Context'])
+ end
+ end
+
+ it 'autocorrects to the right version' do
+ autocorrected = autocorrect_source(source)
+
+ expect(autocorrected).to eq(correct_source)
+ end
+ end
+
+ context 'when `ActionView::Context` is not included' do
+ it 'registers no offense' do
+ inspect_source('include Context')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb
index 86b1ec83f50..7e765659b9d 100644
--- a/spec/services/projects/operations/update_service_spec.rb
+++ b/spec/services/projects/operations/update_service_spec.rb
@@ -11,6 +11,56 @@ describe Projects::Operations::UpdateService do
subject { described_class.new(project, user, params) }
describe '#execute' do
+ context 'metrics dashboard setting' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: 'http://gitlab.com'
+ }
+ }
+ end
+
+ context 'without existing metrics dashboard setting' do
+ it 'creates a setting' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting.external_dashboard_url).to eq(
+ 'http://gitlab.com'
+ )
+ end
+ end
+
+ context 'with existing metrics dashboard setting' do
+ before do
+ create(:project_metrics_setting, project: project)
+ end
+
+ it 'updates the settings' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting.external_dashboard_url).to eq(
+ 'http://gitlab.com'
+ )
+ end
+
+ context 'with blank external_dashboard_url in params' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: ''
+ }
+ }
+ end
+
+ it 'destroys the metrics_setting entry in DB' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting).to be_nil
+ end
+ end
+ end
+ end
+
context 'error tracking' do
context 'with existing error tracking setting' do
let(:params) do
diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/update_deployment_service_spec.rb
index c664bac39fc..7dc52f6816a 100644
--- a/spec/services/update_deployment_service_spec.rb
+++ b/spec/services/update_deployment_service_spec.rb
@@ -22,6 +22,7 @@ describe UpdateDeploymentService do
subject(:service) { described_class.new(deployment) }
before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
job.success! # Create/Succeed deployment
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 2f4e6e4c934..b49d743fb9a 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -61,7 +61,14 @@ module GraphqlHelpers
def variables_for_mutation(name, input)
graphql_input = input.map { |name, value| [GraphqlHelpers.fieldnamerize(name), value] }.to_h
- { input_variable_name_for_mutation(name) => graphql_input }.to_json
+ result = { input_variable_name_for_mutation(name) => graphql_input }
+
+ # Avoid trying to serialize multipart data into JSON
+ if graphql_input.values.none? { |value| io_value?(value) }
+ result.to_json
+ else
+ result
+ end
end
def input_variable_name_for_mutation(mutation_name)
@@ -162,6 +169,10 @@ module GraphqlHelpers
field.arguments.values.any? { |argument| argument.type.non_null? }
end
+ def io_value?(value)
+ Array.wrap(value).any? { |v| v.respond_to?(:to_io) }
+ end
+
def field_type(field)
field_type = field.type
diff --git a/spec/support/protected_branch_helpers.rb b/spec/support/protected_branch_helpers.rb
new file mode 100644
index 00000000000..ede16d1c1e2
--- /dev/null
+++ b/spec/support/protected_branch_helpers.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module ProtectedBranchHelpers
+ def set_allowed_to(operation, option = 'Maintainers', form: '.js-new-protected-branch')
+ within form do
+ select_elem = find(".js-allowed-to-#{operation}")
+ select_elem.click
+
+ wait_for_requests
+
+ within('.dropdown-content') do
+ Array(option).each { |opt| click_on(opt) }
+ end
+
+ # Enhanced select is used in EE, therefore an extra click is needed.
+ select_elem.click if select_elem['aria-expanded'] == 'true'
+ end
+ end
+
+ def set_protected_branch_name(branch_name)
+ find('.js-protected-branch-select').click
+ find('.dropdown-input-field').set(branch_name)
+ click_on("Create wildcard #{branch_name}")
+ end
+
+ def set_defaults
+ set_allowed_to('merge')
+ set_allowed_to('push')
+ end
+end
diff --git a/spec/support/protected_tag_helpers.rb b/spec/support/protected_tag_helpers.rb
new file mode 100644
index 00000000000..fe9be856286
--- /dev/null
+++ b/spec/support/protected_tag_helpers.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require_relative 'protected_branch_helpers'
+
+module ProtectedTagHelpers
+ include ::ProtectedBranchHelpers
+
+ def set_allowed_to(operation, option = 'Maintainers', form: '.new-protected-tag')
+ super
+ end
+
+ def set_protected_tag_name(tag_name)
+ find('.js-protected-tag-select').click
+ find('.dropdown-input-field').set(tag_name)
+ click_on("Create wildcard #{tag_name}")
+ find('.protected-tags-dropdown .dropdown-menu', visible: false)
+ end
+end
diff --git a/spec/support/shared_examples/models/chat_service_spec.rb b/spec/support/shared_examples/models/chat_service_spec.rb
index cf1d52a9616..9d3ce5e2be1 100644
--- a/spec/support/shared_examples/models/chat_service_spec.rb
+++ b/spec/support/shared_examples/models/chat_service_spec.rb
@@ -25,6 +25,12 @@ shared_examples_for "chat service" do |service_name|
end
end
+ describe '.supported_events' do
+ it 'does not support deployment_events' do
+ expect(described_class.supported_events).not_to include('deployment')
+ end
+ end
+
describe "#execute" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
index 940c24c8d67..c31346374f4 100644
--- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
@@ -106,6 +106,14 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(WebMock).to have_requested(:post, webhook_url).once
end
+ it "calls Slack/Mattermost API for deployment events" do
+ deployment_event_data = { object_kind: 'deployment' }
+
+ chat_service.execute(deployment_event_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)
diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb
index 065aeaf2b65..ffe8796ded9 100644
--- a/spec/workers/build_success_worker_spec.rb
+++ b/spec/workers/build_success_worker_spec.rb
@@ -15,6 +15,7 @@ describe BuildSuccessWorker do
let!(:build) { create(:ci_build, :deploy_to_production) }
before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
Deployment.delete_all
build.reload
end
diff --git a/spec/workers/deployments/finished_worker_spec.rb b/spec/workers/deployments/finished_worker_spec.rb
new file mode 100644
index 00000000000..df62821e2cd
--- /dev/null
+++ b/spec/workers/deployments/finished_worker_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::FinishedWorker do
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ before do
+ allow(ProjectServiceWorker).to receive(:perform_async)
+ end
+
+ it 'executes project services for deployment_hooks' do
+ deployment = create(:deployment)
+ project = deployment.project
+ service = create(:service, type: 'SlackService', project: project, deployment_events: true, active: true)
+
+ worker.perform(deployment.id)
+
+ expect(ProjectServiceWorker).to have_received(:perform_async).with(service.id, an_instance_of(Hash))
+ end
+
+ it 'does not execute an inactive service' do
+ deployment = create(:deployment)
+ project = deployment.project
+ create(:service, type: 'SlackService', project: project, deployment_events: true, active: false)
+
+ worker.perform(deployment.id)
+
+ expect(ProjectServiceWorker).not_to have_received(:perform_async)
+ end
+
+ it 'does nothing if a deployment with the given id does not exist' do
+ worker.perform(0)
+
+ expect(ProjectServiceWorker).not_to have_received(:perform_async)
+ end
+ end
+end