summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/admin/projects_controller_spec.rb10
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb89
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb133
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb14
-rw-r--r--spec/controllers/projects/ci/lints_controller_spec.rb123
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb4
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb63
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb13
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb37
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb4
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb18
-rw-r--r--spec/controllers/projects/pipelines_settings_controller_spec.rb28
-rw-r--r--spec/controllers/projects/protected_branches_controller_spec.rb97
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb56
-rw-r--r--spec/controllers/root_controller_spec.rb24
-rw-r--r--spec/factories/appearances.rb17
-rw-r--r--spec/factories/ci/job_artifacts.rb4
-rw-r--r--spec/factories/clusters/clusters.rb4
-rw-r--r--spec/factories/internal_ids.rb7
-rw-r--r--spec/factories/lfs_objects.rb4
-rw-r--r--spec/factories/pages_domains.rb48
-rw-r--r--spec/factories/redirect_routes.rb9
-rw-r--r--spec/factories/uploads.rb5
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb15
-rw-r--r--spec/features/admin/admin_settings_spec.rb121
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb2
-rw-r--r--spec/features/projects/ci/lint_spec.rb (renamed from spec/features/ci_lint_spec.rb)8
-rw-r--r--spec/features/projects/hook_logs/user_reads_log_spec.rb21
-rw-r--r--spec/features/projects/pages_spec.rb91
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb4
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb53
-rw-r--r--spec/features/projects/tree/create_file_spec.rb43
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb14
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb51
-rw-r--r--spec/features/read_only_spec.rb25
-rw-r--r--spec/features/user_can_display_performance_bar_spec.rb10
-rw-r--r--spec/finders/clusters_finder_spec.rb2
-rw-r--r--spec/helpers/preferences_helper_spec.rb4
-rw-r--r--spec/initializers/6_validations_spec.rb14
-rw-r--r--spec/initializers/fog_google_https_private_urls_spec.rb24
-rw-r--r--spec/initializers/settings_spec.rb2
-rw-r--r--spec/javascripts/behaviors/copy_as_gfm_spec.js2
-rw-r--r--spec/javascripts/ci_variable_list/native_form_variable_list_spec.js2
-rw-r--r--spec/javascripts/fixtures/gl_dropdown.html.haml3
-rw-r--r--spec/javascripts/gl_dropdown_spec.js25
-rw-r--r--spec/javascripts/helpers/vue_component_helper.js3
-rw-r--r--spec/javascripts/ide/components/changed_file_icon_spec.js45
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/actions_spec.js35
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js28
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js85
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_spec.js53
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js130
-rw-r--r--spec/javascripts/ide/components/ide_context_bar_spec.js37
-rw-r--r--spec/javascripts/ide/components/ide_external_links_spec.js43
-rw-r--r--spec/javascripts/ide/components/ide_project_tree_spec.js39
-rw-r--r--spec/javascripts/ide/components/ide_repo_tree_spec.js43
-rw-r--r--spec/javascripts/ide/components/ide_side_bar_spec.js42
-rw-r--r--spec/javascripts/ide/components/ide_spec.js41
-rw-r--r--spec/javascripts/ide/components/new_dropdown/index_spec.js84
-rw-r--r--spec/javascripts/ide/components/new_dropdown/modal_spec.js82
-rw-r--r--spec/javascripts/ide/components/new_dropdown/upload_spec.js87
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js173
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js137
-rw-r--r--spec/javascripts/ide/components/repo_file_buttons_spec.js47
-rw-r--r--spec/javascripts/ide/components/repo_file_spec.js80
-rw-r--r--spec/javascripts/ide/components/repo_loading_file_spec.js63
-rw-r--r--spec/javascripts/ide/components/repo_tab_spec.js165
-rw-r--r--spec/javascripts/ide/components/repo_tabs_spec.js81
-rw-r--r--spec/javascripts/ide/helpers.js22
-rw-r--r--spec/javascripts/ide/lib/common/disposable_spec.js44
-rw-r--r--spec/javascripts/ide/lib/common/model_manager_spec.js129
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js113
-rw-r--r--spec/javascripts/ide/lib/decorations/controller_spec.js139
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js196
-rw-r--r--spec/javascripts/ide/lib/diff/diff_spec.js80
-rw-r--r--spec/javascripts/ide/lib/editor_options_spec.js11
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js193
-rw-r--r--spec/javascripts/ide/monaco_loader_spec.js15
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js421
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js172
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js306
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js55
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js505
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js128
-rw-r--r--spec/javascripts/ide/stores/modules/commit/mutations_spec.js42
-rw-r--r--spec/javascripts/ide/stores/mutations/branch_spec.js18
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js157
-rw-r--r--spec/javascripts/ide/stores/mutations/tree_spec.js69
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js79
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js66
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js3
-rw-r--r--spec/javascripts/lib/utils/text_markdown_spec.js10
-rw-r--r--spec/javascripts/merge_request_notes_spec.js3
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js1
-rw-r--r--spec/javascripts/monitoring/dashboard_state_spec.js46
-rw-r--r--spec/javascripts/notes/components/diff_file_header_spec.js2
-rw-r--r--spec/javascripts/notes/components/diff_with_note_spec.js2
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js2
-rw-r--r--spec/javascripts/notes_spec.js313
-rw-r--r--spec/javascripts/pages/labels/components/promote_label_modal_spec.js2
-rw-r--r--spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js2
-rw-r--r--spec/javascripts/performance_bar/components/detailed_metric_spec.js80
-rw-r--r--spec/javascripts/performance_bar/components/performance_bar_app_spec.js88
-rw-r--r--spec/javascripts/performance_bar/components/request_selector_spec.js47
-rw-r--r--spec/javascripts/performance_bar/components/simple_metric_spec.js47
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js89
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js6
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js5
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js33
-rw-r--r--spec/javascripts/vue_shared/components/deprecated_modal_spec.js (renamed from spec/javascripts/vue_shared/components/modal_spec.js)6
-rw-r--r--spec/javascripts/vue_shared/components/markdown/toolbar_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js4
-rw-r--r--spec/lib/backup/manager_spec.rb4
-rw-r--r--spec/lib/backup/repository_spec.rb2
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb21
-rw-r--r--spec/lib/banzai/filter/issuable_state_filter_spec.rb8
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb2
-rw-r--r--spec/lib/gitlab/auth_spec.rb14
-rw-r--r--spec/lib/gitlab/bare_repository_import/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/checks/project_moved_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb153
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb242
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb133
-rw-r--r--spec/lib/gitlab/ci/stage/seed_spec.rb83
-rw-r--r--spec/lib/gitlab/ci/trace/http_io_spec.rb315
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb686
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb137
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb12
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb5
-rw-r--r--spec/lib/gitlab/git/conflict/file_spec.rb50
-rw-r--r--spec/lib/gitlab/git/conflict/parser_spec.rb7
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb5
-rw-r--r--spec/lib/gitlab/git_access_spec.rb59
-rw-r--r--spec/lib/gitlab/gitaly_client/remote_service_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb6
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb6
-rw-r--r--spec/lib/gitlab/http_spec.rb49
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml3
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb65
-rw-r--r--spec/lib/gitlab/profiler_spec.rb24
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb22
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb4
-rw-r--r--spec/lib/gitlab/shell_spec.rb4
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb45
-rw-r--r--spec/lib/gitlab/verify/lfs_objects_spec.rb16
-rw-r--r--spec/lib/gitlab/verify/uploads_spec.rb16
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb11
-rw-r--r--spec/lib/mattermost/command_spec.rb5
-rw-r--r--spec/lib/mattermost/session_spec.rb2
-rw-r--r--spec/lib/mattermost/team_spec.rb5
-rw-r--r--spec/migrations/remove_dot_git_from_usernames_spec.rb4
-rw-r--r--spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb22
-rw-r--r--spec/migrations/remove_empty_fork_networks_spec.rb4
-rw-r--r--spec/models/ci/build_spec.rb59
-rw-r--r--spec/models/ci/job_artifact_spec.rb44
-rw-r--r--spec/models/ci/pipeline_spec.rb339
-rw-r--r--spec/models/concerns/issuable_spec.rb2
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/internal_id_spec.rb106
-rw-r--r--spec/models/issue_spec.rb8
-rw-r--r--spec/models/lfs_object_spec.rb85
-rw-r--r--spec/models/merge_request_spec.rb13
-rw-r--r--spec/models/namespace_spec.rb2
-rw-r--r--spec/models/pages_domain_spec.rb146
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb5
-rw-r--r--spec/models/project_spec.rb51
-rw-r--r--spec/models/repository_spec.rb26
-rw-r--r--spec/models/route_spec.rb159
-rw-r--r--spec/models/user_spec.rb47
-rw-r--r--spec/policies/protected_branch_policy_spec.rb22
-rw-r--r--spec/presenters/project_presenter_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb29
-rw-r--r--spec/requests/api/internal_spec.rb6
-rw-r--r--spec/requests/api/jobs_spec.rb84
-rw-r--r--spec/requests/api/pages_domains_spec.rb14
-rw-r--r--spec/requests/api/projects_spec.rb15
-rw-r--r--spec/requests/api/protected_branches_spec.rb34
-rw-r--r--spec/requests/api/runner_spec.rb70
-rw-r--r--spec/requests/api/search_spec.rb48
-rw-r--r--spec/requests/api/templates_spec.rb2
-rw-r--r--spec/requests/api/v3/builds_spec.rb32
-rw-r--r--spec/requests/api/v3/templates_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb28
-rw-r--r--spec/requests/lfs_http_spec.rb208
-rw-r--r--spec/rubocop/cop/gitlab/httparty_spec.rb74
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb1
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/retry_build_service_spec.rb3
-rw-r--r--spec/services/clusters/create_service_spec.rb2
-rw-r--r--spec/services/groups/transfer_service_spec.rb42
-rw-r--r--spec/services/issues/move_service_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb21
-rw-r--r--spec/services/notification_service_spec.rb51
-rw-r--r--spec/services/projects/create_service_spec.rb2
-rw-r--r--spec/services/projects/fork_service_spec.rb2
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/projects/update_service_spec.rb23
-rw-r--r--spec/services/protected_branches/create_service_spec.rb13
-rw-r--r--spec/services/protected_branches/destroy_service_spec.rb30
-rw-r--r--spec/services/protected_branches/update_service_spec.rb11
-rw-r--r--spec/services/protected_tags/destroy_service_spec.rb17
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/services/verify_pages_domain_service_spec.rb19
-rw-r--r--spec/services/web_hook_service_spec.rb14
-rw-r--r--spec/spec_helper.rb20
-rw-r--r--spec/support/gitaly.rb6
-rw-r--r--spec/support/http_io/http_io_helpers.rb64
-rw-r--r--spec/support/shared_examples/controllers/variables_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_spec.rb40
-rw-r--r--spec/support/shared_examples/uploaders/object_storage_shared_examples.rb126
-rw-r--r--spec/support/stored_repositories.rb2
-rw-r--r--spec/support/stub_configuration.rb8
-rw-r--r--spec/support/stub_object_storage.rb48
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--spec/tasks/gitlab/artifacts/migrate_rake_spec.rb118
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb15
-rw-r--r--spec/tasks/gitlab/cleanup_rake_spec.rb7
-rw-r--r--spec/tasks/gitlab/git_rake_spec.rb7
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb10
-rw-r--r--spec/tasks/gitlab/lfs/migrate_rake_spec.rb37
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/uploads/migrate_rake_spec.rb28
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb22
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb27
-rw-r--r--spec/uploaders/file_mover_spec.rb6
-rw-r--r--spec/uploaders/file_uploader_spec.rb61
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb4
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb35
-rw-r--r--spec/uploaders/legacy_artifact_uploader_spec.rb14
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb58
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb22
-rw-r--r--spec/uploaders/object_storage_spec.rb630
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb22
-rw-r--r--spec/views/projects/ci/lints/show.html.haml_spec.rb (renamed from spec/views/ci/lints/show.html.haml_spec.rb)32
-rw-r--r--spec/views/projects/diffs/_stats.html.haml_spec.rb56
-rw-r--r--spec/views/projects/services/_form.haml_spec.rb46
-rw-r--r--spec/workers/object_storage_upload_worker_spec.rb108
251 files changed, 11093 insertions, 1764 deletions
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb
index d5a3c250f31..cc200b9fed9 100644
--- a/spec/controllers/admin/projects_controller_spec.rb
+++ b/spec/controllers/admin/projects_controller_spec.rb
@@ -31,5 +31,15 @@ describe Admin::ProjectsController do
expect(response.body).not_to match(pending_delete_project.name)
expect(response.body).to match(project.name)
end
+
+ it 'does not have N+1 queries', :use_clean_rails_memory_store_caching, :request_store do
+ get :index
+
+ control_count = ActiveRecord::QueryRecorder.new { get :index }.count
+
+ create(:project)
+
+ expect { get :index }.not_to exceed_query_limit(control_count)
+ end
end
end
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
new file mode 100644
index 00000000000..f4c99ea4064
--- /dev/null
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe SendFileUpload do
+ let(:uploader_class) do
+ Class.new(GitlabUploader) do
+ include ObjectStorage::Concern
+
+ storage_options Gitlab.config.uploads
+
+ private
+
+ # user/:id
+ def dynamic_segment
+ File.join(model.class.to_s.underscore, model.id.to_s)
+ end
+ end
+ end
+
+ let(:controller_class) do
+ Class.new do
+ include SendFileUpload
+ end
+ end
+
+ let(:object) { build_stubbed(:user) }
+ let(:uploader) { uploader_class.new(object, :file) }
+
+ describe '#send_upload' do
+ let(:controller) { controller_class.new }
+ let(:temp_file) { Tempfile.new('test') }
+
+ subject { controller.send_upload(uploader) }
+
+ before do
+ FileUtils.touch(temp_file)
+ end
+
+ after do
+ FileUtils.rm_f(temp_file)
+ end
+
+ context 'when local file is used' do
+ before do
+ uploader.store!(temp_file)
+ end
+
+ it 'sends a file' do
+ expect(controller).to receive(:send_file).with(uploader.path, anything)
+
+ subject
+ end
+ end
+
+ context 'when remote file is used' do
+ before do
+ stub_uploads_object_storage(uploader: uploader_class)
+ uploader.object_store = ObjectStorage::Store::REMOTE
+ uploader.store!(temp_file)
+ end
+
+ context 'and proxying is enabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { true }
+ end
+
+ it 'sends a file' do
+ headers = double
+ expect(headers).to receive(:store).with(Gitlab::Workhorse::SEND_DATA_HEADER, /^send-url:/)
+ expect(controller).to receive(:headers) { headers }
+ expect(controller).to receive(:head).with(:ok)
+
+ subject
+ end
+ end
+
+ context 'and proxying is disabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { false }
+ end
+
+ it 'sends a file' do
+ expect(controller).to receive(:redirect_to).with(/#{uploader.path}/)
+
+ subject
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index c639ad32ec6..5f0e8c5eca9 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -3,72 +3,125 @@ require 'spec_helper'
describe OmniauthCallbacksController do
include LoginHelpers
- let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: provider) }
- let(:provider) { :github }
+ let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
before do
- mock_auth_hash(provider.to_s, 'my-uid', user.email)
+ mock_auth_hash(provider.to_s, extern_uid, user.email)
stub_omniauth_provider(provider, context: request)
end
- it 'allows sign in' do
- post provider
+ context 'when the user is on the last sign in attempt' do
+ let(:extern_uid) { 'my-uid' }
- expect(request.env['warden']).to be_authenticated
- end
+ before do
+ user.update(failed_attempts: User.maximum_attempts.pred)
+ subject.response = ActionDispatch::Response.new
+ end
- shared_context 'sign_up' do
- let(:user) { double(email: 'new@example.com') }
+ context 'when using a form based provider' do
+ let(:provider) { :ldap }
- before do
- stub_omniauth_setting(block_auto_created_users: false)
+ it 'locks the user when sign in fails' do
+ allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
+ request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
+
+ subject.send(:failure)
+
+ expect(user.reload).to be_access_locked
+ end
end
- end
- context 'sign up' do
- include_context 'sign_up'
+ context 'when using a button based provider' do
+ let(:provider) { :github }
- it 'is allowed' do
- post provider
+ it 'does not lock the user when sign in fails' do
+ request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil)
- expect(request.env['warden']).to be_authenticated
+ subject.send(:failure)
+
+ expect(user.reload).not_to be_access_locked
+ end
end
end
- context 'when OAuth is disabled' do
- before do
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- settings = Gitlab::CurrentSettings.current_application_settings
- settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
- end
+ context 'strategies' do
+ context 'github' do
+ let(:extern_uid) { 'my-uid' }
+ let(:provider) { :github }
- it 'prevents login via POST' do
- post provider
+ it 'allows sign in' do
+ post provider
- expect(request.env['warden']).not_to be_authenticated
- end
+ expect(request.env['warden']).to be_authenticated
+ end
- it 'shows warning when attempting login' do
- post provider
+ shared_context 'sign_up' do
+ let(:user) { double(email: 'new@example.com') }
- expect(response).to redirect_to new_user_session_path
- expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
- end
+ before do
+ stub_omniauth_setting(block_auto_created_users: false)
+ end
+ end
+
+ context 'sign up' do
+ include_context 'sign_up'
+
+ it 'is allowed' do
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ end
+ end
+
+ context 'when OAuth is disabled' do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ settings = Gitlab::CurrentSettings.current_application_settings
+ settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
+ end
+
+ it 'prevents login via POST' do
+ post provider
+
+ expect(request.env['warden']).not_to be_authenticated
+ end
- it 'allows linking the disabled provider' do
- user.identities.destroy_all
- sign_in(user)
+ it 'shows warning when attempting login' do
+ post provider
- expect { post provider }.to change { user.reload.identities.count }.by(1)
+ expect(response).to redirect_to new_user_session_path
+ expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
+ end
+
+ it 'allows linking the disabled provider' do
+ user.identities.destroy_all
+ sign_in(user)
+
+ expect { post provider }.to change { user.reload.identities.count }.by(1)
+ end
+
+ context 'sign up' do
+ include_context 'sign_up'
+
+ it 'is prevented' do
+ post provider
+
+ expect(request.env['warden']).not_to be_authenticated
+ end
+ end
+ end
end
- context 'sign up' do
- include_context 'sign_up'
+ context 'auth0' do
+ let(:extern_uid) { '' }
+ let(:provider) { :auth0 }
- it 'is prevented' do
- post provider
+ it 'does not allow sign in without extern_uid' do
+ post 'auth0'
expect(request.env['warden']).not_to be_authenticated
+ expect(response.status).to eq(302)
+ expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
end
end
end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 25a2e13fe1a..4ea6f869aa3 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -145,9 +145,23 @@ describe Projects::ArtifactsController do
context 'when using local file storage' do
it_behaves_like 'a valid file' do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+ let(:store) { ObjectStorage::Store::LOCAL }
let(:archive_path) { JobArtifactUploader.root }
end
end
+
+ context 'when using remote file storage' do
+ before do
+ stub_artifacts_object_storage
+ end
+
+ it_behaves_like 'a valid file' do
+ let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
+ let!(:job) { create(:ci_build, :success, pipeline: pipeline) }
+ let(:store) { ObjectStorage::Store::REMOTE }
+ let(:archive_path) { 'https://' }
+ end
+ end
end
end
diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb
new file mode 100644
index 00000000000..1249a5528a9
--- /dev/null
+++ b/spec/controllers/projects/ci/lints_controller_spec.rb
@@ -0,0 +1,123 @@
+require 'spec_helper'
+
+describe Projects::Ci::LintsController do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #show' do
+ context 'with enough privileges' do
+ before do
+ project.add_developer(user)
+
+ get :show, namespace_id: project.namespace, project_id: project
+ end
+
+ it 'should be success' do
+ expect(response).to be_success
+ end
+
+ it 'should render show page' do
+ expect(response).to render_template :show
+ end
+
+ it 'should retrieve project' do
+ expect(assigns(:project)).to eq(project)
+ end
+ end
+
+ context 'without enough privileges' do
+ before do
+ project.add_guest(user)
+
+ get :show, namespace_id: project.namespace, project_id: project
+ end
+
+ it 'should respond with 404' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST #create' do
+ let(:remote_file_path) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
+
+ let(:remote_file_content) do
+ <<~HEREDOC
+ before_script:
+ - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
+ - ruby -v
+ - which ruby
+ - gem install bundler --no-ri --no-rdoc
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
+ HEREDOC
+ end
+
+ let(:content) do
+ <<~HEREDOC
+ include:
+ - #{remote_file_path}
+
+ rubocop:
+ script:
+ - bundle exec rubocop
+ HEREDOC
+ end
+
+ context 'with a valid gitlab-ci.yml' do
+ before do
+ WebMock.stub_request(:get, remote_file_path).to_return(body: remote_file_content)
+ project.add_developer(user)
+
+ post :create, namespace_id: project.namespace, project_id: project, content: content
+ end
+
+ it 'should be success' do
+ expect(response).to be_success
+ end
+
+ it 'render show page' do
+ expect(response).to render_template :show
+ end
+
+ it 'should retrieve project' do
+ expect(assigns(:project)).to eq(project)
+ end
+ end
+
+ context 'with an invalid gitlab-ci.yml' do
+ let(:content) do
+ <<~HEREDOC
+ rubocop:
+ scriptt:
+ - bundle exec rubocop
+ HEREDOC
+ end
+
+ before do
+ project.add_developer(user)
+
+ post :create, namespace_id: project.namespace, project_id: project, content: content
+ end
+
+ it 'should assign errors' do
+ expect(assigns[:error]).to eq('jobs:rubocop config contains unknown keys: scriptt')
+ end
+ end
+
+ context 'without enough privileges' do
+ before do
+ project.add_guest(user)
+
+ post :create, namespace_id: project.namespace, project_id: project, content: content
+ end
+
+ it 'should respond with 404' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 15ce418d0d6..82b20e12850 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -18,7 +18,7 @@ describe Projects::ClustersController do
context 'when project has one or more clusters' do
let(:project) { create(:project) }
let!(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- let!(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
+ let!(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, :production_environment, projects: [project]) }
it 'lists available clusters' do
go
@@ -32,7 +32,7 @@ describe Projects::ClustersController do
before do
allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
- create_list(:cluster, 2, :provided_by_gcp, projects: [project])
+ create_list(:cluster, 2, :provided_by_gcp, :production_environment, projects: [project])
get :index, namespace_id: project.namespace, project_id: project, page: last_page
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index f3e303bb0fe..31046c202e6 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -1,7 +1,9 @@
+# coding: utf-8
require 'spec_helper'
describe Projects::JobsController do
include ApiHelpers
+ include HttpIOHelpers
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_pipeline, project: project) }
@@ -203,6 +205,41 @@ describe Projects::JobsController do
end
end
+ context 'when trace artifact is in ObjectStorage' do
+ let!(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
+
+ before do
+ allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
+ allow_any_instance_of(JobArtifactUploader).to receive(:url) { remote_trace_url }
+ allow_any_instance_of(JobArtifactUploader).to receive(:size) { remote_trace_size }
+ end
+
+ context 'when there are no network issues' do
+ before do
+ stub_remote_trace_206
+
+ get_trace
+ end
+
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['html']).to eq(job.trace.html)
+ end
+ end
+
+ context 'when there is a network issue' do
+ before do
+ stub_remote_trace_500
+ end
+
+ it 'returns a trace' do
+ expect { get_trace }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError)
+ end
+ end
+ end
+
def get_trace
get :trace, namespace_id: project.namespace,
project_id: project,
@@ -446,14 +483,18 @@ describe Projects::JobsController do
end
describe 'GET raw' do
- before do
- get_raw
+ subject do
+ post :raw, namespace_id: project.namespace,
+ project_id: project,
+ id: job.id
end
context 'when job has a trace artifact' do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'returns a trace' do
+ response = subject
+
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain; charset=utf-8'
expect(response.body).to eq job.job_artifacts_trace.open.read
@@ -464,6 +505,8 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
it 'send a trace file' do
+ response = subject
+
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain; charset=utf-8'
expect(response.body).to eq 'BUILD TRACE'
@@ -474,14 +517,22 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'returns not_found' do
+ response = subject
+
expect(response).to have_gitlab_http_status(:not_found)
end
end
- def get_raw
- post :raw, namespace_id: project.namespace,
- project_id: project,
- id: job.id
+ context 'when the trace artifact is in ObjectStorage' do
+ let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+ before do
+ allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
+ end
+
+ it 'redirect to the trace file url' do
+ expect(subject).to redirect_to(job.job_artifacts_trace.file.url)
+ end
end
end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 306094f7ffb..5b2614163ff 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -20,14 +20,23 @@ describe Projects::MilestonesController do
describe "#show" do
render_views
- def view_milestone
- get :show, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+ def view_milestone(options = {})
+ params = { namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid }
+ get :show, params.merge(options)
end
it 'shows milestone page' do
view_milestone
expect(response).to have_gitlab_http_status(200)
+ expect(response.content_type).to eq 'text/html'
+ end
+
+ it 'returns milestone json' do
+ view_milestone format: :json
+
+ expect(response).to have_http_status(404)
+ expect(response.content_type).to eq 'application/json'
end
end
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 4705c50de7e..11f54eef531 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -65,4 +65,41 @@ describe Projects::PagesController do
end
end
end
+
+ describe 'PATCH update' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ project: { pages_https_only: false }
+ }
+ end
+
+ let(:update_service) { double(execute: { status: :success }) }
+
+ before do
+ allow(Projects::UpdateService).to receive(:new) { update_service }
+ end
+
+ it 'returns 302 status' do
+ patch :update, request_params
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+
+ it 'redirects back to the pages settings' do
+ patch :update, request_params
+
+ expect(response).to redirect_to(project_pages_path(project))
+ end
+
+ it 'calls the update service' do
+ expect(Projects::UpdateService)
+ .to receive(:new)
+ .with(project, user, request_params[:project])
+ .and_return(update_service)
+
+ patch :update, request_params
+ end
+ end
end
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
index 83a3799e883..d4058a5c515 100644
--- a/spec/controllers/projects/pages_domains_controller_spec.rb
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::PagesDomainsController do
end
let(:pages_domain_params) do
- build(:pages_domain, :with_certificate, :with_key, domain: 'my.otherdomain.com').slice(:key, :certificate, :domain)
+ build(:pages_domain, domain: 'my.otherdomain.com').slice(:key, :certificate, :domain)
end
before do
@@ -68,7 +68,7 @@ describe Projects::PagesDomainsController do
end
let(:pages_domain_params) do
- attributes_for(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate)
+ attributes_for(:pages_domain).slice(:key, :certificate)
end
let(:params) do
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 966ffdf6996..3506305f755 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -80,7 +80,7 @@ describe Projects::PipelineSchedulesController do
context 'when variables_attributes has one variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', value: 'AAA123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' }]
})
end
@@ -101,7 +101,8 @@ describe Projects::PipelineSchedulesController do
context 'when variables_attributes has two variables and duplicated' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', value: 'AAA123' }, { key: 'AAA', value: 'BBB123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' },
+ { key: 'AAA', secret_value: 'BBB123' }]
})
end
@@ -152,7 +153,7 @@ describe Projects::PipelineSchedulesController do
context 'when params include one variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', value: 'AAA123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' }]
})
end
@@ -169,7 +170,8 @@ describe Projects::PipelineSchedulesController do
context 'when params include two duplicated variables' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', value: 'AAA123' }, { key: 'AAA', value: 'BBB123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' },
+ { key: 'AAA', secret_value: 'BBB123' }]
})
end
@@ -194,7 +196,7 @@ describe Projects::PipelineSchedulesController do
context 'when adds a new variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', value: 'AAA123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' }]
})
end
@@ -209,7 +211,7 @@ describe Projects::PipelineSchedulesController do
context 'when adds a new duplicated variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'CCC', value: 'AAA123' }]
+ variables_attributes: [{ key: 'CCC', secret_value: 'AAA123' }]
})
end
@@ -224,7 +226,7 @@ describe Projects::PipelineSchedulesController do
context 'when updates a variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ id: pipeline_schedule_variable.id, value: 'new_value' }]
+ variables_attributes: [{ id: pipeline_schedule_variable.id, secret_value: 'new_value' }]
})
end
@@ -252,7 +254,7 @@ describe Projects::PipelineSchedulesController do
let(:schedule) do
basic_param.merge({
variables_attributes: [{ id: pipeline_schedule_variable.id, _destroy: true },
- { key: 'CCC', value: 'CCC123' }]
+ { key: 'CCC', secret_value: 'CCC123' }]
})
end
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb
index 1cc488bef32..913b9bd804a 100644
--- a/spec/controllers/projects/pipelines_settings_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb
@@ -47,10 +47,32 @@ describe Projects::PipelinesSettingsController do
expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(true)
end
- it 'queues a CreatePipelineWorker' do
- expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
+ context 'when the project repository is empty' do
+ it 'sets a warning flash' do
+ expect(subject).to set_flash[:warning]
+ end
- subject
+ it 'does not queue a CreatePipelineWorker' do
+ expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
+
+ subject
+ end
+ end
+
+ context 'when the project repository is not empty' do
+ let(:project) { create(:project, :repository) }
+
+ it 'sets a success flash' do
+ allow(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
+
+ expect(subject).to set_flash[:success]
+ end
+
+ it 'queues a CreatePipelineWorker' do
+ expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
+
+ subject
+ end
end
end
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index 80be135b5d8..096e29bc39f 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -1,6 +1,16 @@
require('spec_helper')
describe Projects::ProtectedBranchesController do
+ let(:project) { create(:project, :repository) }
+ let(:protected_branch) { create(:protected_branch, project: project) }
+ let(:project_params) { { namespace_id: project.namespace.to_param, project_id: project } }
+ let(:base_params) { project_params.merge(id: protected_branch.id) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ end
+
describe "GET #index" do
let(:project) { create(:project_empty_repo, :public) }
@@ -8,4 +18,91 @@ describe Projects::ProtectedBranchesController do
get(:index, namespace_id: project.namespace.to_param, project_id: project)
end
end
+
+ describe "POST #create" do
+ let(:master_access_level) { [{ access_level: Gitlab::Access::MASTER }] }
+ let(:access_level_params) do
+ { merge_access_levels_attributes: master_access_level,
+ push_access_levels_attributes: master_access_level }
+ end
+ let(:create_params) { attributes_for(:protected_branch).merge(access_level_params) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'creates the protected branch rule' do
+ expect do
+ post(:create, project_params.merge(protected_branch: create_params))
+ end.to change(ProtectedBranch, :count).by(1)
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ allow(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents creation of the protected branch rule" do
+ post(:create, project_params.merge(protected_branch: create_params))
+
+ expect(ProtectedBranch.count).to eq 0
+ end
+ end
+ end
+
+ describe "PUT #update" do
+ let(:update_params) { { name: 'new_name' } }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'updates the protected branch rule' do
+ put(:update, base_params.merge(protected_branch: update_params))
+
+ expect(protected_branch.reload.name).to eq('new_name')
+ expect(json_response["name"]).to eq('new_name')
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ allow(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents update of the protected branch rule" do
+ old_name = protected_branch.name
+
+ put(:update, base_params.merge(protected_branch: update_params))
+
+ expect(protected_branch.reload.name).to eq(old_name)
+ end
+ end
+ end
+
+ describe "DELETE #destroy" do
+ before do
+ sign_in(user)
+ end
+
+ it "deletes the protected branch rule" do
+ delete(:destroy, base_params)
+
+ expect { ProtectedBranch.find(protected_branch.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ allow(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ delete(:destroy, base_params)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index b7df42168e0..08e2ccf893a 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -8,10 +8,7 @@ describe Projects::RawController do
let(:id) { 'master/README.md' }
it 'delivers ASCII file' do
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ get_show(public_project, id)
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
@@ -25,10 +22,7 @@ describe Projects::RawController do
let(:id) { 'master/files/images/6049019_460s.jpg' }
it 'sets image content type header' do
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ get_show(public_project, id)
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
@@ -54,21 +48,40 @@ describe Projects::RawController do
it 'serves the file' do
expect(controller).to receive(:send_file).with("#{LfsObjectUploader.root}/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment')
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ get_show(public_project, id)
expect(response).to have_gitlab_http_status(200)
end
+
+ context 'and lfs uses object storage' do
+ before do
+ lfs_object.file = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png")
+ lfs_object.save!
+ stub_lfs_object_storage
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
+
+ it 'responds with redirect to file' do
+ get_show(public_project, id)
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(response.location).to include(lfs_object.reload.file.path)
+ end
+
+ it 'sets content disposition' do
+ get_show(public_project, id)
+
+ file_uri = URI.parse(response.location)
+ params = CGI.parse(file_uri.query)
+
+ expect(params["response-content-disposition"].first).to eq 'attachment;filename="lfs_object.iso"'
+ end
+ end
end
context 'when project does not have access' do
it 'does not serve the file' do
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ get_show(public_project, id)
expect(response).to have_gitlab_http_status(404)
end
@@ -81,10 +94,7 @@ describe Projects::RawController do
end
it 'delivers ASCII file' do
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ get_show(public_project, id)
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
@@ -95,4 +105,10 @@ describe Projects::RawController do
end
end
end
+
+ def get_show(project, id)
+ get(:show, namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: id)
+ end
end
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index b32eb39b1fb..7688538a468 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -90,6 +90,30 @@ describe RootController do
end
end
+ context 'who has customized their dashboard setting for assigned issues' do
+ before do
+ user.dashboard = 'issues'
+ end
+
+ it 'redirects to their assigned issues' do
+ get :index
+
+ expect(response).to redirect_to issues_dashboard_path(assignee_id: user.id)
+ end
+ end
+
+ context 'who has customized their dashboard setting for assigned merge requests' do
+ before do
+ user.dashboard = 'merge_requests'
+ end
+
+ it 'redirects to their assigned merge requests' do
+ get :index
+
+ expect(response).to redirect_to merge_requests_dashboard_path(assignee_id: user.id)
+ end
+ end
+
context 'who uses the default dashboard setting' do
it 'renders the default dashboard' do
get :index
diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb
index 5f9c57c0c8d..18c7453bd1b 100644
--- a/spec/factories/appearances.rb
+++ b/spec/factories/appearances.rb
@@ -2,8 +2,21 @@
FactoryBot.define do
factory :appearance do
- title "MepMep"
- description "This is my Community Edition instance"
+ title "GitLab Community Edition"
+ description "Open source software to collaborate on code"
new_project_guidelines "Custom project guidelines"
end
+
+ trait :with_logo do
+ logo { fixture_file_upload('spec/fixtures/dk.png') }
+ end
+
+ trait :with_header_logo do
+ header_logo { fixture_file_upload('spec/fixtures/dk.png') }
+ end
+
+ trait :with_logos do
+ with_logo
+ with_header_logo
+ end
end
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 8544d54ccaa..3d3287d8168 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -5,6 +5,10 @@ FactoryBot.define do
job factory: :ci_build
file_type :archive
+ trait :remote_store do
+ file_store JobArtifactUploader::Store::REMOTE
+ end
+
after :build do |artifact|
artifact.project ||= artifact.job.project
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 20d5580f0c2..98566f907f9 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -32,5 +32,9 @@ FactoryBot.define do
trait :disabled do
enabled false
end
+
+ trait :production_environment do
+ sequence(:environment_scope) { |n| "production#{n}/*" }
+ end
end
end
diff --git a/spec/factories/internal_ids.rb b/spec/factories/internal_ids.rb
new file mode 100644
index 00000000000..fbde07a391a
--- /dev/null
+++ b/spec/factories/internal_ids.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :internal_id do
+ project
+ usage :issues
+ last_value { project.issues.maximum(:iid) || 0 }
+ end
+end
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
index caaed4d5246..eaf3a4ed497 100644
--- a/spec/factories/lfs_objects.rb
+++ b/spec/factories/lfs_objects.rb
@@ -15,4 +15,8 @@ FactoryBot.define do
trait :correct_oid do
oid 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'
end
+
+ trait :object_storage do
+ file_store { LfsObjectUploader::Store::REMOTE }
+ end
end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index 35b44e1c52e..20671da016e 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -4,25 +4,7 @@ FactoryBot.define do
verified_at { Time.now }
enabled_until { 1.week.from_now }
- trait :disabled do
- verified_at nil
- enabled_until nil
- end
-
- trait :unverified do
- verified_at nil
- end
-
- trait :reverify do
- enabled_until { 1.hour.from_now }
- end
-
- trait :expired do
- enabled_until { 1.hour.ago }
- end
-
- trait :with_certificate do
- certificate '-----BEGIN CERTIFICATE-----
+ certificate '-----BEGIN CERTIFICATE-----
MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
@@ -36,10 +18,8 @@ joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
YHi2yesCrOvVXt+lgPTd
-----END CERTIFICATE-----'
- end
- trait :with_key do
- key '-----BEGIN PRIVATE KEY-----
+ key '-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
@@ -55,6 +35,30 @@ EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
nNp/xedE1YxutQ==
-----END PRIVATE KEY-----'
+
+ trait :disabled do
+ verified_at nil
+ enabled_until nil
+ end
+
+ trait :unverified do
+ verified_at nil
+ end
+
+ trait :reverify do
+ enabled_until { 1.hour.from_now }
+ end
+
+ trait :expired do
+ enabled_until { 1.hour.ago }
+ end
+
+ trait :without_certificate do
+ certificate nil
+ end
+
+ trait :without_key do
+ key nil
end
trait :with_missing_chain do
diff --git a/spec/factories/redirect_routes.rb b/spec/factories/redirect_routes.rb
index c29c81c5df9..774232d0b34 100644
--- a/spec/factories/redirect_routes.rb
+++ b/spec/factories/redirect_routes.rb
@@ -2,14 +2,5 @@ FactoryBot.define do
factory :redirect_route do
sequence(:path) { |n| "redirect#{n}" }
source factory: :group
- permanent false
-
- trait :permanent do
- permanent true
- end
-
- trait :temporary do
- permanent false
- end
end
end
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index ff3a2a76acc..b45f6f30e40 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -5,6 +5,7 @@ FactoryBot.define do
uploader "AvatarUploader"
mount_point :avatar
secret nil
+ store ObjectStorage::Store::LOCAL
# we should build a mount agnostic upload by default
transient do
@@ -27,6 +28,10 @@ FactoryBot.define do
secret SecureRandom.hex
end
+ trait :object_storage do
+ store ObjectStorage::Store::REMOTE
+ end
+
trait :namespace_upload do
model { build(:group) }
path { File.join(secret, filename) }
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index 9ea3cfa72c6..9946cc77d1d 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -55,14 +55,19 @@ feature 'Admin disables Git access protocol' do
end
def disable_http_protocol
- visit admin_application_settings_path
- find('#application_setting_enabled_git_access_protocol').find(:xpath, 'option[2]').select_option
- click_on 'Save'
+ switch_git_protocol(2)
end
def disable_ssh_protocol
+ switch_git_protocol(3)
+ end
+
+ def switch_git_protocol(value)
visit admin_application_settings_path
- find('#application_setting_enabled_git_access_protocol').find(:xpath, 'option[3]').select_option
- click_on 'Save'
+
+ page.within('.as-visibility-access') do
+ find('#application_setting_enabled_git_access_protocol').find(:xpath, "option[#{value}]").select_option
+ click_on 'Save'
+ end
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 39b213988f0..c89bc54cad4 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -10,18 +10,21 @@ feature 'Admin updates settings' do
end
scenario 'Change visibility settings' do
- choose "application_setting_default_project_visibility_20"
- click_button 'Save'
+ page.within('.as-visibility-access') do
+ choose "application_setting_default_project_visibility_20"
+ click_button 'Save changes'
+ end
expect(page).to have_content "Application settings saved successfully"
end
scenario 'Uncheck all restricted visibility levels' do
- find('#application_setting_visibility_level_0').set(false)
- find('#application_setting_visibility_level_10').set(false)
- find('#application_setting_visibility_level_20').set(false)
-
- click_button 'Save'
+ page.within('.as-visibility-access') do
+ find('#application_setting_visibility_level_0').set(false)
+ find('#application_setting_visibility_level_10').set(false)
+ find('#application_setting_visibility_level_20').set(false)
+ click_button 'Save changes'
+ end
expect(page).to have_content "Application settings saved successfully"
expect(find('#application_setting_visibility_level_0')).not_to be_checked
@@ -29,34 +32,94 @@ feature 'Admin updates settings' do
expect(find('#application_setting_visibility_level_20')).not_to be_checked
end
- scenario 'Change application settings' do
- uncheck 'Gravatar enabled'
- fill_in 'Home page URL', with: 'https://about.gitlab.com/'
- fill_in 'Help page text', with: 'Example text'
- check 'Hide marketing-related entries from help'
- fill_in 'Support page URL', with: 'http://example.com/help'
- uncheck 'Project export enabled'
- click_button 'Save'
+ scenario 'Change Visibility and Access Controls' do
+ page.within('.as-visibility-access') do
+ uncheck 'Project export enabled'
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.project_export_enabled).to be_falsey
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change Account and Limit Settings' do
+ page.within('.as-account-limit') do
+ uncheck 'Gravatar enabled'
+ click_button 'Save changes'
+ end
expect(Gitlab::CurrentSettings.gravatar_enabled).to be_falsey
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change Sign-in restrictions' do
+ page.within('.as-signin') do
+ fill_in 'Home page URL', with: 'https://about.gitlab.com/'
+ click_button 'Save changes'
+ end
+
expect(Gitlab::CurrentSettings.home_page_url).to eq "https://about.gitlab.com/"
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change Help page' do
+ page.within('.as-help-page') do
+ fill_in 'Help page text', with: 'Example text'
+ check 'Hide marketing-related entries from help'
+ fill_in 'Support page URL', with: 'http://example.com/help'
+ click_button 'Save changes'
+ end
+
expect(Gitlab::CurrentSettings.help_page_text).to eq "Example text"
expect(Gitlab::CurrentSettings.help_page_hide_commercial_content).to be_truthy
expect(Gitlab::CurrentSettings.help_page_support_url).to eq "http://example.com/help"
- expect(Gitlab::CurrentSettings.project_export_enabled).to be_falsey
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change AutoDevOps settings' do
- check 'Enabled Auto DevOps (Beta) for projects by default'
- fill_in 'Auto devops domain', with: 'domain.com'
- click_button 'Save'
+ scenario 'Change Pages settings' do
+ page.within('.as-pages') do
+ fill_in 'Maximum size of pages (MB)', with: 15
+ check 'Require users to prove ownership of custom domains'
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.max_pages_size).to eq 15
+ expect(Gitlab::CurrentSettings.pages_domain_verification_enabled?).to be_truthy
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change CI/CD settings' do
+ page.within('.as-ci-cd') do
+ check 'Enabled Auto DevOps (Beta) for projects by default'
+ fill_in 'Auto devops domain', with: 'domain.com'
+ click_button 'Save changes'
+ end
expect(Gitlab::CurrentSettings.auto_devops_enabled?).to be true
expect(Gitlab::CurrentSettings.auto_devops_domain).to eq('domain.com')
expect(page).to have_content "Application settings saved successfully"
end
+ scenario 'Change Influx settings' do
+ page.within('.as-influx') do
+ check 'Enable InfluxDB Metrics'
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.metrics_enabled?).to be true
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change Prometheus settings' do
+ page.within('.as-prometheus') do
+ check 'Enable Prometheus Metrics'
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.prometheus_metrics_enabled?).to be true
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
scenario 'Change Slack Notifications Service template settings' do
first(:link, 'Service Templates').click
click_link 'Slack notifications'
@@ -83,18 +146,22 @@ feature 'Admin updates settings' do
context 'sign-in restrictions', :js do
it 'de-activates oauth sign-in source' do
- find('input#application_setting_enabled_oauth_sign_in_sources_[value=gitlab]').send_keys(:return)
+ page.within('.as-signin') do
+ find('input#application_setting_enabled_oauth_sign_in_sources_[value=gitlab]').send_keys(:return)
- expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active')
+ expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active')
+ end
end
end
scenario 'Change Keys settings' do
- select 'Are forbidden', from: 'RSA SSH keys'
- select 'Are allowed', from: 'DSA SSH keys'
- select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
- select 'Are forbidden', from: 'ED25519 SSH keys'
- click_on 'Save'
+ page.within('.as-visibility-access') do
+ select 'Are forbidden', from: 'RSA SSH keys'
+ select 'Are allowed', from: 'DSA SSH keys'
+ select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
+ select 'Are forbidden', from: 'ED25519 SSH keys'
+ click_on 'Save changes'
+ end
forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE.to_s
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index f82ed6300cc..4d897f09b57 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -20,7 +20,7 @@ describe 'Copy as GFM', :js do
end
# The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
- # The handlers defined in app/assets/javascripts/copy_as_gfm.js consequently convert that same HTML to GFM.
+ # The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js consequently convert that same HTML to GFM.
# To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
# by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/projects/ci/lint_spec.rb
index 220b934154e..313950072e7 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/projects/ci/lint_spec.rb
@@ -1,10 +1,14 @@
require 'spec_helper'
describe 'CI Lint', :js do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
before do
- sign_in(create(:user))
+ project.add_developer(user)
+ sign_in(user)
- visit ci_lint_path
+ visit project_ci_lint_path(project)
find('#ci-editor')
execute_script("ace.edit('ci-editor').setValue(#{yaml_content.to_json});")
diff --git a/spec/features/projects/hook_logs/user_reads_log_spec.rb b/spec/features/projects/hook_logs/user_reads_log_spec.rb
new file mode 100644
index 00000000000..18e975fa653
--- /dev/null
+++ b/spec/features/projects/hook_logs/user_reads_log_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'Hook logs' do
+ given(:web_hook_log) { create(:web_hook_log, response_body: '<script>') }
+ given(:project) { web_hook_log.web_hook.project }
+ given(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+
+ sign_in(user)
+ end
+
+ scenario 'user reads log without getting XSS' do
+ visit(
+ project_hook_hook_log_path(
+ project, web_hook_log.web_hook, web_hook_log))
+
+ expect(page).to have_content('<script>')
+ end
+end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index 233d2e67b9d..bdd49f731c7 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -40,11 +40,6 @@ feature 'Pages' do
end
context 'when support for external domains is disabled' do
- before do
- allow(Gitlab.config.pages).to receive(:external_http).and_return(nil)
- allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
- end
-
it 'renders message that support is disabled' do
visit project_pages_path(project)
@@ -52,7 +47,9 @@ feature 'Pages' do
end
end
- context 'when pages are exposed on external HTTP address' do
+ context 'when pages are exposed on external HTTP address', :http_pages_enabled do
+ given(:project) { create(:project, pages_https_only: false) }
+
shared_examples 'adds new domain' do
it 'adds new domain' do
visit new_project_pages_domain_path(project)
@@ -64,11 +61,6 @@ feature 'Pages' do
end
end
- before do
- allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
- allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
- end
-
it 'allows to add new domain' do
visit project_pages_path(project)
@@ -80,13 +72,13 @@ feature 'Pages' do
context 'when project in group namespace' do
it_behaves_like 'adds new domain' do
let(:group) { create :group }
- let(:project) { create :project, namespace: group }
+ let(:project) { create(:project, namespace: group, pages_https_only: false) }
end
end
context 'when pages domain is added' do
before do
- project.pages_domains.create!(domain: 'my.test.domain.com')
+ create(:pages_domain, project: project, domain: 'my.test.domain.com')
visit new_project_pages_domain_path(project)
end
@@ -104,7 +96,7 @@ feature 'Pages' do
end
end
- context 'when pages are exposed on external HTTPS address' do
+ context 'when pages are exposed on external HTTPS address', :https_pages_enabled do
let(:certificate_pem) do
<<~PEM
-----BEGIN CERTIFICATE-----
@@ -145,11 +137,6 @@ feature 'Pages' do
KEY
end
- before do
- allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
- allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443'])
- end
-
it 'adds new domain with certificate' do
visit new_project_pages_domain_path(project)
@@ -163,7 +150,7 @@ feature 'Pages' do
describe 'updating the certificate for an existing domain' do
let!(:domain) do
- create(:pages_domain, :with_key, :with_certificate, project: project)
+ create(:pages_domain, project: project)
end
it 'allows the certificate to be updated' do
@@ -237,6 +224,70 @@ feature 'Pages' do
it_behaves_like 'no pages deployed'
end
+ describe 'HTTPS settings', :js, :https_pages_enabled do
+ background do
+ project.namespace.update(owner: user)
+
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ scenario 'tries to change the setting' do
+ visit project_pages_path(project)
+ expect(page).to have_content("Force domains with SSL certificates to use HTTPS")
+
+ uncheck :project_pages_https_only
+
+ click_button 'Save'
+
+ expect(page).to have_text('Your changes have been saved')
+ expect(page).not_to have_checked_field('project_pages_https_only')
+ end
+
+ context 'setting could not be updated' do
+ let(:service) { instance_double('Projects::UpdateService') }
+
+ before do
+ allow(Projects::UpdateService).to receive(:new).and_return(service)
+ allow(service).to receive(:execute).and_return(status: :error)
+ end
+
+ scenario 'tries to change the setting' do
+ visit project_pages_path(project)
+
+ uncheck :project_pages_https_only
+
+ click_button 'Save'
+
+ expect(page).to have_text('Something went wrong on our end')
+ end
+ end
+
+ context 'non-HTTPS domain exists' do
+ given(:project) { create(:project, pages_https_only: false) }
+
+ before do
+ create(:pages_domain, :without_key, :without_certificate, project: project)
+ end
+
+ scenario 'the setting is disabled' do
+ visit project_pages_path(project)
+
+ expect(page).to have_field(:project_pages_https_only, disabled: true)
+ expect(page).not_to have_button('Save')
+ end
+ end
+
+ context 'HTTPS pages are disabled', :https_pages_disabled do
+ scenario 'the setting is unavailable' do
+ visit project_pages_path(project)
+
+ expect(page).not_to have_field(:project_pages_https_only)
+ expect(page).not_to have_content('Force domains with SSL certificates to use HTTPS')
+ expect(page).not_to have_button('Save')
+ end
+ end
+ end
+
describe 'Remove page' do
context 'when user is the owner' do
let(:project) { create :project, :repository }
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 65e24862d43..065d00d51d4 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -160,9 +160,9 @@ feature 'Pipeline Schedules', :js do
click_link 'New schedule'
fill_in_schedule_form
all('[name="schedule[variables_attributes][][key]"]')[0].set('AAA')
- all('[name="schedule[variables_attributes][][value]"]')[0].set('AAA123')
+ all('[name="schedule[variables_attributes][][secret_value]"]')[0].set('AAA123')
all('[name="schedule[variables_attributes][][key]"]')[1].set('BBB')
- all('[name="schedule[variables_attributes][][value]"]')[1].set('BBB123')
+ all('[name="schedule[variables_attributes][][secret_value]"]')[1].set('BBB123')
save_pipeline_schedule
end
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
new file mode 100644
index 00000000000..d96c7e655ba
--- /dev/null
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+feature 'Multi-file editor new directory', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+
+ click_link('Web IDE')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
+ end
+
+ it 'creates directory in current directory' do
+ find('.add-to-tree').click
+
+ click_link('New directory')
+
+ page.within('.modal') do
+ find('.form-control').set('folder name')
+
+ click_button('Create directory')
+ end
+
+ find('.add-to-tree').click
+
+ click_link('New file')
+
+ page.within('.modal-dialog') do
+ find('.form-control').set('file name')
+
+ click_button('Create file')
+ end
+
+ wait_for_requests
+
+ fill_in('commit-message', with: 'commit message ide')
+
+ click_button('Commit')
+
+ expect(page).to have_content('folder name')
+ end
+end
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
new file mode 100644
index 00000000000..a4cbd5cf766
--- /dev/null
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+feature 'Multi-file editor new file', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_path(project)
+
+ wait_for_requests
+
+ click_link('Web IDE')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
+ end
+
+ it 'creates file in current directory' do
+ find('.add-to-tree').click
+
+ click_link('New file')
+
+ page.within('.modal') do
+ find('.form-control').set('file name')
+
+ click_button('Create file')
+ end
+
+ wait_for_requests
+
+ fill_in('commit-message', with: 'commit message ide')
+
+ click_button('Commit')
+
+ expect(page).to have_content('file name')
+ end
+end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index c8a17871508..c4b3fb9d171 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -25,4 +25,18 @@ feature 'Projects tree' do
expect(page).to have_selector('.label-lfs', text: 'LFS')
end
end
+
+ context 'web IDE', :js do
+ before do
+ visit project_tree_path(project, File.join('master', 'bar'))
+
+ click_link 'Web IDE'
+
+ find('.ide-file-list')
+ end
+
+ it 'opens folder in IDE' do
+ expect(page).to have_selector('.is-open', text: 'bar')
+ end
+ end
end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
new file mode 100644
index 00000000000..8e53ae15700
--- /dev/null
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+feature 'Multi-file editor upload file', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') }
+ let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+
+ click_link('Web IDE')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
+ end
+
+ it 'uploads text file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', txt_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
+ expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
+ end
+
+ it 'uploads image file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', img_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
+ expect(page).not_to have_selector('.monaco-editor')
+ end
+end
diff --git a/spec/features/read_only_spec.rb b/spec/features/read_only_spec.rb
new file mode 100644
index 00000000000..8bfaf558466
--- /dev/null
+++ b/spec/features/read_only_spec.rb
@@ -0,0 +1,25 @@
+require 'rails_helper'
+
+describe 'read-only message' do
+ set(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'shows read-only banner when database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+
+ visit root_dashboard_path
+
+ expect(page).to have_content('You are on a read-only GitLab instance.')
+ end
+
+ it 'does not show read-only banner when database is able to read-write' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(false)
+
+ visit root_dashboard_path
+
+ expect(page).not_to have_content('You are on a read-only GitLab instance.')
+ end
+end
diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb
index 975c157bcf5..e069c2fddd1 100644
--- a/spec/features/user_can_display_performance_bar_spec.rb
+++ b/spec/features/user_can_display_performance_bar_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe 'User can display performance bar', :js do
shared_examples 'performance bar cannot be displayed' do
it 'does not show the performance bar by default' do
- expect(page).not_to have_css('#peek')
+ expect(page).not_to have_css('#js-peek')
end
context 'when user press `pb`' do
@@ -12,14 +12,14 @@ describe 'User can display performance bar', :js do
end
it 'does not show the performance bar by default' do
- expect(page).not_to have_css('#peek')
+ expect(page).not_to have_css('#js-peek')
end
end
end
shared_examples 'performance bar can be displayed' do
it 'does not show the performance bar by default' do
- expect(page).not_to have_css('#peek')
+ expect(page).not_to have_css('#js-peek')
end
context 'when user press `pb`' do
@@ -28,7 +28,7 @@ describe 'User can display performance bar', :js do
end
it 'shows the performance bar' do
- expect(page).to have_css('#peek')
+ expect(page).to have_css('#js-peek')
end
end
end
@@ -41,7 +41,7 @@ describe 'User can display performance bar', :js do
it 'shows the performance bar by default' do
refresh # Because we're stubbing Rails.env after the 1st visit to root_path
- expect(page).to have_css('#peek')
+ expect(page).to have_css('#js-peek')
end
end
diff --git a/spec/finders/clusters_finder_spec.rb b/spec/finders/clusters_finder_spec.rb
index c10efac2432..da529e0670f 100644
--- a/spec/finders/clusters_finder_spec.rb
+++ b/spec/finders/clusters_finder_spec.rb
@@ -6,7 +6,7 @@ describe ClustersFinder do
describe '#execute' do
let(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- let(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
+ let(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, :production_environment, projects: [project]) }
subject { described_class.new(project, user, scope).execute }
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index e2a0c4322ff..c9d2ec8a4ae 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -21,7 +21,9 @@ describe PreferencesHelper do
["Your Projects' Activity", 'project_activity'],
["Starred Projects' Activity", 'starred_project_activity'],
["Your Groups", 'groups'],
- ["Your Todos", 'todos']
+ ["Your Todos", 'todos'],
+ ["Assigned Issues", 'issues'],
+ ["Assigned Merge Requests", 'merge_requests']
]
end
end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 83283f03940..1dc307ea922 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -15,7 +15,7 @@ describe '6_validations' do
describe 'validate_storages_config' do
context 'with correct settings' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/d'))
end
it 'passes through' do
@@ -25,7 +25,7 @@ describe '6_validations' do
context 'when one of the settings is incorrect' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c', 'failure_count_threshold' => 'not a number' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c', 'failure_count_threshold' => 'not a number'))
end
it 'throws an error' do
@@ -35,7 +35,7 @@ describe '6_validations' do
context 'with invalid storage names' do
before do
- mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' })
+ mock_storages('name with spaces' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'))
end
it 'throws an error' do
@@ -67,7 +67,7 @@ describe '6_validations' do
describe 'validate_storages_paths' do
context 'with correct settings' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/d'))
end
it 'passes through' do
@@ -77,7 +77,7 @@ describe '6_validations' do
context 'with nested storage paths' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c/d' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c/d'))
end
it 'throws an error' do
@@ -87,7 +87,7 @@ describe '6_validations' do
context 'with similar but un-nested storage paths' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c2' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c2'))
end
it 'passes through' do
@@ -97,7 +97,7 @@ describe '6_validations' do
describe 'inaccessible storage' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/a/path/that/does/not/exist' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/a/path/that/does/not/exist'))
end
it 'passes through with a warning' do
diff --git a/spec/initializers/fog_google_https_private_urls_spec.rb b/spec/initializers/fog_google_https_private_urls_spec.rb
new file mode 100644
index 00000000000..de3c157ab7b
--- /dev/null
+++ b/spec/initializers/fog_google_https_private_urls_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe 'Fog::Storage::GoogleXML::File' do
+ let(:storage) do
+ Fog.mock!
+ Fog::Storage.new({
+ google_storage_access_key_id: "asdf",
+ google_storage_secret_access_key: "asdf",
+ provider: "Google"
+ })
+ end
+
+ let(:file) do
+ directory = storage.directories.create(key: 'data')
+ directory.files.create(
+ body: 'Hello World!',
+ key: 'hello_world.txt'
+ )
+ end
+
+ it 'delegates to #get_https_url' do
+ expect(file.url(Time.now)).to start_with("https://")
+ end
+end
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index 838ca9fabef..57f5adbbc40 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -1,5 +1,5 @@
require 'spec_helper'
-require_relative '../../config/initializers/1_settings'
+require_relative '../../config/initializers/1_settings' unless defined?(Settings)
describe Settings do
describe '#ldap' do
diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js
index b8155144e2a..efbe09a10a2 100644
--- a/spec/javascripts/behaviors/copy_as_gfm_spec.js
+++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js
@@ -1,4 +1,4 @@
-import { CopyAsGFM } from '~/behaviors/copy_as_gfm';
+import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
describe('CopyAsGFM', () => {
describe('CopyAsGFM.pasteGFM', () => {
diff --git a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
index 1ea8d86cb7e..94a0c999d66 100644
--- a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
@@ -20,7 +20,7 @@ describe('NativeFormVariableList', () => {
it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
const $row = $wrapper.find('.js-row');
expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('schedule[variables_attributes][][key]');
- expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('schedule[variables_attributes][][value]');
+ expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('schedule[variables_attributes][][secret_value]');
$wrapper.closest('form').trigger('trigger-submit');
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
index a20390c08ee..43d57c2c4dc 100644
--- a/spec/javascripts/fixtures/gl_dropdown.html.haml
+++ b/spec/javascripts/fixtures/gl_dropdown.html.haml
@@ -1,7 +1,8 @@
%div
.dropdown.inline
%button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
- Projects
+ .dropdown-toggle-text
+ Projects
%i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 0e4a7017406..5393502196e 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -256,4 +256,29 @@ describe('glDropdown', function describeDropdown() {
});
});
});
+
+ it('should keep selected item after selecting a second time', () => {
+ const options = {
+ isSelectable(item, $el) {
+ return !$el.hasClass('is-active');
+ },
+ toggleLabel(item) {
+ return item && item.id;
+ },
+ };
+ initDropDown.call(this, false, false, options);
+ const $item = $(`${ITEM_SELECTOR}:first() a`, this.$dropdownMenuElement);
+
+ // select item the first time
+ this.dropdownButtonElement.click();
+ $item.click();
+ expect($item).toHaveClass('is-active');
+ // select item the second time
+ this.dropdownButtonElement.click();
+ $item.click();
+ expect($item).toHaveClass('is-active');
+
+ expect($('.dropdown-toggle-text')).toHaveText(this.projectsData[0].id.toString());
+ });
});
+
diff --git a/spec/javascripts/helpers/vue_component_helper.js b/spec/javascripts/helpers/vue_component_helper.js
new file mode 100644
index 00000000000..257c9f5526a
--- /dev/null
+++ b/spec/javascripts/helpers/vue_component_helper.js
@@ -0,0 +1,3 @@
+export default function removeBreakLine (data) {
+ return data.replace(/\r?\n|\r/g, ' ');
+}
diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js
new file mode 100644
index 00000000000..987aea7befc
--- /dev/null
+++ b/spec/javascripts/ide/components/changed_file_icon_spec.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import changedFileIcon from '~/ide/components/changed_file_icon.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('IDE changed file icon', () => {
+ let vm;
+
+ beforeEach(() => {
+ const component = Vue.extend(changedFileIcon);
+
+ vm = createComponent(component, {
+ file: {
+ tempFile: false,
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('changedIcon', () => {
+ it('equals file-modified when not a temp file', () => {
+ expect(vm.changedIcon).toBe('file-modified');
+ });
+
+ it('equals file-addition when a temp file', () => {
+ vm.file.tempFile = true;
+
+ expect(vm.changedIcon).toBe('file-addition');
+ });
+ });
+
+ describe('changedIconClass', () => {
+ it('includes multi-file-modified when not a temp file', () => {
+ expect(vm.changedIconClass).toContain('multi-file-modified');
+ });
+
+ it('includes multi-file-addition when a temp file', () => {
+ vm.file.tempFile = true;
+
+ expect(vm.changedIconClass).toContain('multi-file-addition');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
new file mode 100644
index 00000000000..144e78d14b5
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import commitActions from '~/ide/components/commit_sidebar/actions.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from 'spec/ide/helpers';
+
+describe('IDE commit sidebar actions', () => {
+ let vm;
+
+ beforeEach(done => {
+ const Component = Vue.extend(commitActions);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.currentBranchId = 'master';
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders 3 groups', () => {
+ expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(3);
+ });
+
+ it('renders current branch text', () => {
+ expect(vm.$el.textContent).toContain('Commit to master branch');
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
new file mode 100644
index 00000000000..5b402886b55
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list collapsed', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(listCollapsed);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.changedFiles.push(file('file1'), file('file2'));
+ vm.$store.state.changedFiles[0].tempFile = true;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders added & modified files count', () => {
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1');
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
new file mode 100644
index 00000000000..15b66952d99
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import listItem from '~/ide/components/commit_sidebar/list_item.vue';
+import router from '~/ide/ide_router';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list item', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(listItem);
+
+ f = file('test-file');
+
+ vm = mountComponent(Component, {
+ file: f,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file path', () => {
+ expect(
+ vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim(),
+ ).toBe(f.path);
+ });
+
+ it('calls discardFileChanges when clicking discard button', () => {
+ spyOn(vm, 'discardFileChanges');
+
+ vm.$el.querySelector('.multi-file-discard-btn').click();
+
+ expect(vm.discardFileChanges).toHaveBeenCalled();
+ });
+
+ it('opens a closed file in the editor when clicking the file path', () => {
+ spyOn(vm, 'openFileInEditor').and.callThrough();
+ spyOn(vm, 'updateViewer');
+ spyOn(router, 'push');
+
+ vm.$el.querySelector('.multi-file-commit-list-path').click();
+
+ expect(vm.openFileInEditor).toHaveBeenCalled();
+ expect(router.push).toHaveBeenCalled();
+ });
+
+ it('calls updateViewer with diff when clicking file', () => {
+ spyOn(vm, 'openFileInEditor').and.callThrough();
+ spyOn(vm, 'updateViewer');
+ spyOn(router, 'push');
+
+ vm.$el.querySelector('.multi-file-commit-list-path').click();
+
+ expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+ });
+
+ describe('computed', () => {
+ describe('iconName', () => {
+ it('returns modified when not a tempFile', () => {
+ expect(vm.iconName).toBe('file-modified');
+ });
+
+ it('returns addition when not a tempFile', () => {
+ f.tempFile = true;
+
+ expect(vm.iconName).toBe('file-addition');
+ });
+ });
+
+ describe('iconClass', () => {
+ it('returns modified when not a tempFile', () => {
+ expect(vm.iconClass).toContain('multi-file-modified');
+ });
+
+ it('returns addition when not a tempFile', () => {
+ f.tempFile = true;
+
+ expect(vm.iconClass).toContain('multi-file-addition');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
new file mode 100644
index 00000000000..a62c0a28340
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(commitSidebarList);
+
+ vm = createComponentWithStore(Component, store, {
+ title: 'Staged',
+ fileList: [],
+ });
+
+ vm.$store.state.rightPanelCollapsed = false;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with a list of files', () => {
+ beforeEach(done => {
+ const f = file('file name');
+ f.changed = true;
+ vm.fileList.push(f);
+
+ Vue.nextTick(done);
+ });
+
+ it('renders list', () => {
+ expect(vm.$el.querySelectorAll('li').length).toBe(1);
+ });
+ });
+
+ describe('collapsed', () => {
+ beforeEach(done => {
+ vm.$store.state.rightPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('hides list', () => {
+ expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
+ expect(vm.$el.querySelector('.help-block')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
new file mode 100644
index 00000000000..4e8243439f3
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
@@ -0,0 +1,130 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from 'spec/ide/helpers';
+
+describe('IDE commit sidebar radio group', () => {
+ let vm;
+
+ beforeEach(done => {
+ const Component = Vue.extend(radioGroup);
+
+ store.state.commit.commitAction = '2';
+
+ vm = createComponentWithStore(Component, store, {
+ value: '1',
+ label: 'test',
+ checked: true,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('uses label if present', () => {
+ expect(vm.$el.textContent).toContain('test');
+ });
+
+ it('uses slot if label is not present', done => {
+ vm.$destroy();
+
+ vm = new Vue({
+ components: {
+ radioGroup,
+ },
+ store,
+ template: `
+ <radio-group
+ value="1"
+ >
+ Testing slot
+ </radio-group>
+ `,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.textContent).toContain('Testing slot');
+
+ done();
+ });
+ });
+
+ it('updates store when changing radio button', done => {
+ vm.$el.querySelector('input').dispatchEvent(new Event('change'));
+
+ Vue.nextTick(() => {
+ expect(store.state.commit.commitAction).toBe('1');
+
+ done();
+ });
+ });
+
+ it('renders helpText tooltip', done => {
+ vm.helpText = 'help text';
+
+ Vue.nextTick(() => {
+ const help = vm.$el.querySelector('.help-block');
+
+ expect(help).not.toBeNull();
+ expect(help.getAttribute('data-original-title')).toBe('help text');
+
+ done();
+ });
+ });
+
+ describe('with input', () => {
+ beforeEach(done => {
+ vm.$destroy();
+
+ const Component = Vue.extend(radioGroup);
+
+ store.state.commit.commitAction = '1';
+
+ vm = createComponentWithStore(Component, store, {
+ value: '1',
+ label: 'test',
+ checked: true,
+ showInput: true,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders input box when commitAction matches value', () => {
+ expect(vm.$el.querySelector('.form-control')).not.toBeNull();
+ });
+
+ it('hides input when commitAction doesnt match value', done => {
+ store.state.commit.commitAction = '2';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.form-control')).toBeNull();
+ done();
+ });
+ });
+
+ it('updates branch name in store on input', done => {
+ const input = vm.$el.querySelector('.form-control');
+ input.value = 'testing-123';
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick(() => {
+ expect(store.state.commit.newBranchName).toBe('testing-123');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_context_bar_spec.js b/spec/javascripts/ide/components/ide_context_bar_spec.js
new file mode 100644
index 00000000000..e17b051f137
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_context_bar_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideContextBar from '~/ide/components/ide_context_bar.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+
+describe('Multi-file editor right context bar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideContextBar);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'svg',
+ });
+
+ vm.$store.state.rightPanelCollapsed = false;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed', () => {
+ beforeEach(done => {
+ vm.$store.state.rightPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('adds collapsed class', () => {
+ expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_external_links_spec.js b/spec/javascripts/ide/components/ide_external_links_spec.js
new file mode 100644
index 00000000000..9f6cb459f3b
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_external_links_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import ideExternalLinks from '~/ide/components/ide_external_links.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('ide external links component', () => {
+ let vm;
+ let fakeReferrer;
+ let Component;
+
+ const fakeProjectUrl = '/project/';
+
+ beforeEach(() => {
+ Component = Vue.extend(ideExternalLinks);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('goBackUrl', () => {
+ it('renders the Go Back link with the referrer when present', () => {
+ fakeReferrer = '/example/README.md';
+ spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
+
+ vm = createComponent(Component, {
+ projectUrl: fakeProjectUrl,
+ }).$mount();
+
+ expect(vm.goBackUrl).toEqual(fakeReferrer);
+ });
+
+ it('renders the Go Back link with the project url when referrer is not present', () => {
+ fakeReferrer = '';
+ spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
+
+ vm = createComponent(Component, {
+ projectUrl: fakeProjectUrl,
+ }).$mount();
+
+ expect(vm.goBackUrl).toEqual(fakeProjectUrl);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_project_tree_spec.js b/spec/javascripts/ide/components/ide_project_tree_spec.js
new file mode 100644
index 00000000000..657682cb39c
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_project_tree_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import ProjectTree from '~/ide/components/ide_project_tree.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('IDE project tree', () => {
+ const Component = Vue.extend(ProjectTree);
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent(Component, {
+ project: {
+ id: 1,
+ name: 'test',
+ web_url: gl.TEST_HOST,
+ avatar_url: '',
+ branches: [],
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders identicon when projct has no avatar', () => {
+ expect(vm.$el.querySelector('.identicon')).not.toBeNull();
+ });
+
+ it('renders avatar image if project has avatar', done => {
+ vm.project.avatar_url = gl.TEST_HOST;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.identicon')).toBeNull();
+ expect(vm.$el.querySelector('img.avatar')).not.toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_repo_tree_spec.js b/spec/javascripts/ide/components/ide_repo_tree_spec.js
new file mode 100644
index 00000000000..e0fbc90ca61
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_repo_tree_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
+import createComponent from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('IdeRepoTree', () => {
+ let vm;
+ let tree;
+
+ beforeEach(() => {
+ const IdeRepoTree = Vue.extend(ideRepoTree);
+
+ tree = {
+ tree: [file()],
+ loading: false,
+ };
+
+ vm = createComponent(IdeRepoTree, {
+ tree,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a sidebar', () => {
+ expect(vm.$el.querySelector('.loading-file')).toBeNull();
+ expect(vm.$el.querySelector('.file')).not.toBeNull();
+ });
+
+ it('renders 3 loading files if tree is loading', done => {
+ tree.loading = true;
+
+ vm.$nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.multi-file-loading-container').length,
+ ).toEqual(3);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_side_bar_spec.js b/spec/javascripts/ide/components/ide_side_bar_spec.js
new file mode 100644
index 00000000000..699dae1ce2f
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_side_bar_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideSidebar from '~/ide/components/ide_side_bar.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+
+describe('IdeSidebar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideSidebar);
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders a sidebar', () => {
+ expect(
+ vm.$el.querySelector('.multi-file-commit-panel-inner'),
+ ).not.toBeNull();
+ });
+
+ it('renders loading icon component', done => {
+ vm.$store.state.loading = true;
+
+ vm.$nextTick(() => {
+ expect(
+ vm.$el.querySelector('.multi-file-loading-container'),
+ ).not.toBeNull();
+ expect(
+ vm.$el.querySelectorAll('.multi-file-loading-container').length,
+ ).toBe(3);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
new file mode 100644
index 00000000000..5bd890094cc
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ide from '~/ide/components/ide.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file, resetStore } from '../helpers';
+
+describe('ide component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ide);
+
+ vm = createComponentWithStore(Component, store, {
+ emptyStateSvgPath: 'svg',
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'svg',
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('does not render panel right when no files open', () => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
+ });
+
+ it('renders panel right when files are open', done => {
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [file()],
+ };
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js
new file mode 100644
index 00000000000..e08abe7d849
--- /dev/null
+++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import newDropdown from '~/ide/components/new_dropdown/index.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('new dropdown component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const component = Vue.extend(newDropdown);
+
+ vm = createComponentWithStore(component, store, {
+ branch: 'master',
+ path: '',
+ });
+
+ vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.path = '';
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders new file, upload and new directory links', () => {
+ expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
+ expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe(
+ 'Upload file',
+ );
+ expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe(
+ 'New directory',
+ );
+ });
+
+ describe('createNewItem', () => {
+ it('sets modalType to blob when new file is clicked', () => {
+ vm.$el.querySelectorAll('a')[0].click();
+
+ expect(vm.modalType).toBe('blob');
+ });
+
+ it('sets modalType to tree when new directory is clicked', () => {
+ vm.$el.querySelectorAll('a')[2].click();
+
+ expect(vm.modalType).toBe('tree');
+ });
+
+ it('opens modal when link is clicked', done => {
+ vm.$el.querySelectorAll('a')[0].click();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.modal')).not.toBeNull();
+
+ done();
+ });
+ });
+ });
+
+ describe('hideModal', () => {
+ beforeAll(done => {
+ vm.openModal = true;
+ Vue.nextTick(done);
+ });
+
+ it('closes modal after toggling', done => {
+ vm.hideModal();
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.modal')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
new file mode 100644
index 00000000000..a6e1e5a0d35
--- /dev/null
+++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+import modal from '~/ide/components/new_dropdown/modal.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('new file modal component', () => {
+ const Component = Vue.extend(modal);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ ['tree', 'blob'].forEach(type => {
+ describe(type, () => {
+ beforeEach(() => {
+ vm = createComponent(Component, {
+ type,
+ branchId: 'master',
+ path: '',
+ });
+
+ vm.entryName = 'testing';
+ });
+
+ it(`sets modal title as ${type}`, () => {
+ const title = type === 'tree' ? 'directory' : 'file';
+
+ expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(
+ `Create new ${title}`,
+ );
+ });
+
+ it(`sets button label as ${type}`, () => {
+ const title = type === 'tree' ? 'directory' : 'file';
+
+ expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(
+ `Create ${title}`,
+ );
+ });
+
+ it(`sets form label as ${type}`, () => {
+ const title = type === 'tree' ? 'Directory' : 'File';
+
+ expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(
+ `${title} name`,
+ );
+ });
+
+ describe('createEntryInStore', () => {
+ it('$emits create', () => {
+ spyOn(vm, '$emit');
+
+ vm.createEntryInStore();
+
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ branchId: 'master',
+ name: 'testing',
+ type,
+ });
+ });
+ });
+ });
+ });
+
+ it('focuses field on mount', () => {
+ document.body.innerHTML += '<div class="js-test"></div>';
+
+ vm = createComponent(
+ Component,
+ {
+ type: 'tree',
+ branchId: 'master',
+ path: '',
+ },
+ '.js-test',
+ );
+
+ expect(document.activeElement).toBe(vm.$refs.fieldName);
+
+ vm.$el.remove();
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
new file mode 100644
index 00000000000..2bc5d701601
--- /dev/null
+++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
@@ -0,0 +1,87 @@
+import Vue from 'vue';
+import upload from '~/ide/components/new_dropdown/upload.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('new dropdown upload', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(upload);
+
+ vm = createComponent(Component, {
+ branchId: 'master',
+ path: '',
+ });
+
+ vm.entryName = 'testing';
+
+ spyOn(vm, '$emit');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('readFile', () => {
+ beforeEach(() => {
+ spyOn(FileReader.prototype, 'readAsText');
+ spyOn(FileReader.prototype, 'readAsDataURL');
+ });
+
+ it('calls readAsText for text files', () => {
+ const file = {
+ type: 'text/html',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file);
+ });
+
+ it('calls readAsDataURL for non-text files', () => {
+ const file = {
+ type: 'images/png',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
+ });
+ });
+
+ describe('createFile', () => {
+ const target = {
+ result: 'content',
+ };
+ const binaryTarget = {
+ result: 'base64,base64content',
+ };
+ const file = {
+ name: 'file',
+ };
+
+ it('creates new file', () => {
+ vm.createFile(target, file, true);
+
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ name: file.name,
+ branchId: 'master',
+ type: 'blob',
+ content: target.result,
+ base64: false,
+ });
+ });
+
+ it('splits content on base64 if binary', () => {
+ vm.createFile(binaryTarget, file, false);
+
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ name: file.name,
+ branchId: 'master',
+ type: 'blob',
+ content: binaryTarget.result.split('base64,')[1],
+ base64: true,
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
new file mode 100644
index 00000000000..113ade269e9
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -0,0 +1,173 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import repoCommitSection from '~/ide/components/repo_commit_section.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
+import { file, resetStore } from '../helpers';
+
+describe('RepoCommitSection', () => {
+ let vm;
+
+ function createComponent() {
+ const Component = Vue.extend(repoCommitSection);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'commitsvg',
+ });
+
+ vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.currentBranchId = 'master';
+ vm.$store.state.projects.abcproject = {
+ web_url: '',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+
+ vm.$store.state.rightPanelCollapsed = false;
+ vm.$store.state.currentBranch = 'master';
+ vm.$store.state.changedFiles = [file('file1'), file('file2')];
+ vm.$store.state.changedFiles.forEach(f =>
+ Object.assign(f, {
+ changed: true,
+ content: 'testing',
+ }),
+ );
+
+ return vm.$mount();
+ }
+
+ beforeEach(done => {
+ vm = createComponent();
+
+ spyOn(service, 'getTreeData').and.returnValue(
+ Promise.resolve({
+ headers: {
+ 'page-title': 'test',
+ },
+ json: () =>
+ Promise.resolve({
+ last_commit_path: 'last_commit_path',
+ parent_tree_url: 'parent_tree_url',
+ path: '/',
+ trees: [{ name: 'tree' }],
+ blobs: [{ name: 'blob' }],
+ submodules: [{ name: 'submodule' }],
+ }),
+ }),
+ );
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('empty Stage', () => {
+ it('renders no changes text', () => {
+ resetStore(vm.$store);
+ const Component = Vue.extend(repoCommitSection);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'nochangessvg',
+ committedStateSvgPath: 'svg',
+ }).$mount();
+
+ expect(
+ vm.$el.querySelector('.js-empty-state').textContent.trim(),
+ ).toContain('No changes');
+ expect(
+ vm.$el.querySelector('.js-empty-state img').getAttribute('src'),
+ ).toBe('nochangessvg');
+ });
+ });
+
+ it('renders a commit section', () => {
+ const changedFileElements = [
+ ...vm.$el.querySelectorAll('.multi-file-commit-list li'),
+ ];
+ const submitCommit = vm.$el.querySelector('form .btn');
+
+ expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
+ expect(changedFileElements.length).toEqual(2);
+
+ changedFileElements.forEach((changedFile, i) => {
+ expect(changedFile.textContent.trim()).toContain(
+ vm.$store.state.changedFiles[i].path,
+ );
+ });
+
+ expect(submitCommit.disabled).toBeTruthy();
+ expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
+ });
+
+ it('updates commitMessage in store on input', done => {
+ const textarea = vm.$el.querySelector('textarea');
+
+ textarea.value = 'testing commit message';
+
+ textarea.dispatchEvent(new Event('input'));
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(vm.$store.state.commit.commitMessage).toBe(
+ 'testing commit message',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('discard draft button', () => {
+ it('hidden when commitMessage is empty', () => {
+ expect(
+ vm.$el.querySelector('.multi-file-commit-form .btn-default'),
+ ).toBeNull();
+ });
+
+ it('resets commitMessage when clicking discard button', done => {
+ vm.$store.state.commit.commitMessage = 'testing commit message';
+
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.multi-file-commit-form .btn-default').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.$store.state.commit.commitMessage).not.toBe(
+ 'testing commit message',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('when submitting', () => {
+ beforeEach(() => {
+ spyOn(vm, 'commitChanges');
+ });
+
+ it('calls commitChanges', done => {
+ vm.$store.state.commit.commitMessage = 'testing commit message';
+
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.multi-file-commit-form .btn-success').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.commitChanges).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
new file mode 100644
index 00000000000..ae657e8c881
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -0,0 +1,137 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoEditor from '~/ide/components/repo_editor.vue';
+import monacoLoader from '~/ide/monaco_loader';
+import Editor from '~/ide/lib/editor';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../helpers';
+
+describe('RepoEditor', () => {
+ let vm;
+
+ beforeEach(done => {
+ const f = file();
+ const RepoEditor = Vue.extend(repoEditor);
+
+ vm = createComponentWithStore(RepoEditor, store, {
+ file: f,
+ });
+
+ f.active = true;
+ f.tempFile = true;
+ f.html = 'testing';
+ vm.$store.state.openFiles.push(f);
+ vm.$store.state.entries[f.path] = f;
+ vm.monaco = true;
+
+ vm.$mount();
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ setTimeout(done, 0);
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+
+ Editor.editorInstance.modelManager.dispose();
+ });
+
+ it('renders an ide container', done => {
+ Vue.nextTick(() => {
+ expect(vm.shouldHideEditor).toBeFalsy();
+
+ done();
+ });
+ });
+
+ describe('when open file is binary and not raw', () => {
+ beforeEach(done => {
+ vm.file.binary = true;
+
+ vm.$nextTick(done);
+ });
+
+ it('does not render the IDE', () => {
+ expect(vm.shouldHideEditor).toBeTruthy();
+ });
+
+ it('shows activeFile html', () => {
+ expect(vm.$el.textContent).toContain('testing');
+ });
+ });
+
+ describe('createEditorInstance', () => {
+ it('calls createInstance when viewer is editor', done => {
+ spyOn(vm.editor, 'createInstance');
+
+ vm.createEditorInstance();
+
+ vm.$nextTick(() => {
+ expect(vm.editor.createInstance).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('calls createDiffInstance when viewer is diff', done => {
+ vm.$store.state.viewer = 'diff';
+
+ spyOn(vm.editor, 'createDiffInstance');
+
+ vm.createEditorInstance();
+
+ vm.$nextTick(() => {
+ expect(vm.editor.createDiffInstance).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('setupEditor', () => {
+ it('creates new model', () => {
+ spyOn(vm.editor, 'createModel').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file);
+ expect(vm.model).not.toBeNull();
+ });
+
+ it('attaches model to editor', () => {
+ spyOn(vm.editor, 'attachModel').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model);
+ });
+
+ it('adds callback methods', () => {
+ spyOn(vm.editor, 'onPositionChange').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.onPositionChange).toHaveBeenCalled();
+ expect(vm.model.events.size).toBe(1);
+ });
+
+ it('updates state when model content changed', done => {
+ vm.model.setValue('testing 123');
+
+ setTimeout(() => {
+ expect(vm.file.content).toBe('testing 123');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_file_buttons_spec.js b/spec/javascripts/ide/components/repo_file_buttons_spec.js
new file mode 100644
index 00000000000..c86bdb132b4
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_file_buttons_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import repoFileButtons from '~/ide/components/repo_file_buttons.vue';
+import createVueComponent from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('RepoFileButtons', () => {
+ const activeFile = file();
+ let vm;
+
+ function createComponent() {
+ const RepoFileButtons = Vue.extend(repoFileButtons);
+
+ activeFile.rawPath = 'test';
+ activeFile.blamePath = 'test';
+ activeFile.commitsPath = 'test';
+
+ return createVueComponent(RepoFileButtons, {
+ file: activeFile,
+ });
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders Raw, Blame, History, Permalink and Preview toggle', done => {
+ vm = createComponent();
+
+ vm.$nextTick(() => {
+ const raw = vm.$el.querySelector('.raw');
+ const blame = vm.$el.querySelector('.blame');
+ const history = vm.$el.querySelector('.history');
+
+ expect(raw.href).toMatch(`/${activeFile.rawPath}`);
+ expect(raw.textContent.trim()).toEqual('Raw');
+ expect(blame.href).toMatch(`/${activeFile.blamePath}`);
+ expect(blame.textContent.trim()).toEqual('Blame');
+ expect(history.href).toMatch(`/${activeFile.commitsPath}`);
+ expect(history.textContent.trim()).toEqual('History');
+ expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual(
+ 'Permalink',
+ );
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js
new file mode 100644
index 00000000000..ff391cb4351
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_file_spec.js
@@ -0,0 +1,80 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoFile from '~/ide/components/repo_file.vue';
+import router from '~/ide/ide_router';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('RepoFile', () => {
+ let vm;
+
+ function createComponent(propsData) {
+ const RepoFile = Vue.extend(repoFile);
+
+ vm = createComponentWithStore(RepoFile, store, propsData);
+
+ vm.$mount();
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders link, icon and name', () => {
+ createComponent({
+ file: file('t4'),
+ level: 0,
+ });
+
+ const name = vm.$el.querySelector('.ide-file-name');
+
+ expect(name.href).toMatch('');
+ expect(name.textContent.trim()).toEqual(vm.file.name);
+ });
+
+ it('fires clickFile when the link is clicked', done => {
+ spyOn(router, 'push');
+ createComponent({
+ file: file('t3'),
+ level: 0,
+ });
+
+ vm.$el.querySelector('.file-name').click();
+
+ setTimeout(() => {
+ expect(router.push).toHaveBeenCalledWith(`/project${vm.file.url}`);
+
+ done();
+ });
+ });
+
+ describe('locked file', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file('locked file');
+ f.file_lock = {
+ user: {
+ name: 'testuser',
+ updated_at: new Date(),
+ },
+ };
+
+ createComponent({
+ file: f,
+ level: 0,
+ });
+ });
+
+ it('renders lock icon', () => {
+ expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
+ });
+
+ it('renders a tooltip', () => {
+ expect(
+ vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset
+ .originalTitle,
+ ).toContain('Locked by testuser');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_loading_file_spec.js b/spec/javascripts/ide/components/repo_loading_file_spec.js
new file mode 100644
index 00000000000..8f9644216bc
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_loading_file_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoLoadingFile from '~/ide/components/repo_loading_file.vue';
+import { resetStore } from '../helpers';
+
+describe('RepoLoadingFile', () => {
+ let vm;
+
+ function createComponent() {
+ const RepoLoadingFile = Vue.extend(repoLoadingFile);
+
+ return new RepoLoadingFile({
+ store,
+ }).$mount();
+ }
+
+ function assertLines(lines) {
+ lines.forEach((line, n) => {
+ const index = n + 1;
+ expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy();
+ });
+ }
+
+ function assertColumns(columns) {
+ columns.forEach(column => {
+ const container = column.querySelector('.animation-container');
+ const lines = [...container.querySelectorAll(':scope > div')];
+
+ expect(container).toBeTruthy();
+ expect(lines.length).toEqual(6);
+ assertLines(lines);
+ });
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders 3 columns of animated LoC', () => {
+ vm = createComponent();
+ const columns = [...vm.$el.querySelectorAll('td')];
+
+ expect(columns.length).toEqual(3);
+ assertColumns(columns);
+ });
+
+ it('renders 1 column of animated LoC if isMini', done => {
+ vm = createComponent();
+ vm.$store.state.leftPanelCollapsed = true;
+ vm.$store.state.openFiles.push('test');
+
+ vm.$nextTick(() => {
+ const columns = [...vm.$el.querySelectorAll('td')];
+
+ expect(columns.length).toEqual(1);
+ assertColumns(columns);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
new file mode 100644
index 00000000000..ddb5204e3a7
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_tab_spec.js
@@ -0,0 +1,165 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoTab from '~/ide/components/repo_tab.vue';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../helpers';
+
+describe('RepoTab', () => {
+ let vm;
+
+ function createComponent(propsData) {
+ const RepoTab = Vue.extend(repoTab);
+
+ return new RepoTab({
+ store,
+ propsData,
+ }).$mount();
+ }
+
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders a close link and a name link', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+ vm.$store.state.openFiles.push(vm.tab);
+ const close = vm.$el.querySelector('.multi-file-tab-close');
+ const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`);
+
+ expect(close.innerHTML).toContain('#close');
+ expect(name.textContent.trim()).toEqual(vm.tab.name);
+ });
+
+ it('fires clickFile when the link is clicked', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'clickFile');
+
+ vm.$el.click();
+
+ expect(vm.clickFile).toHaveBeenCalledWith(vm.tab);
+ });
+
+ it('calls closeFile when clicking close button', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'closeFile');
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ expect(vm.closeFile).toHaveBeenCalledWith(vm.tab.path);
+ });
+
+ it('changes icon on hover', done => {
+ const tab = file();
+ tab.changed = true;
+ vm = createComponent({
+ tab,
+ });
+
+ vm.$el.dispatchEvent(new Event('mouseover'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.multi-file-modified')).toBeNull();
+
+ vm.$el.dispatchEvent(new Event('mouseout'));
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.$el.querySelector('.multi-file-modified')).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ describe('locked file', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file('locked file');
+ f.file_lock = {
+ user: {
+ name: 'testuser',
+ updated_at: new Date(),
+ },
+ };
+
+ vm = createComponent({
+ tab: f,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders lock icon', () => {
+ expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
+ });
+
+ it('renders a tooltip', () => {
+ expect(
+ vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle,
+ ).toContain('Locked by testuser');
+ });
+ });
+
+ describe('methods', () => {
+ describe('closeTab', () => {
+ it('closes tab if file has changed', done => {
+ const tab = file();
+ tab.changed = true;
+ tab.opened = true;
+ vm = createComponent({
+ tab,
+ });
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.state.changedFiles.push(tab);
+ vm.$store.state.entries[tab.path] = tab;
+ vm.$store.dispatch('setFileActive', tab.path);
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
+ expect(vm.$store.state.changedFiles.length).toBe(1);
+
+ done();
+ });
+ });
+
+ it('closes tab when clicking close btn', done => {
+ const tab = file('lose');
+ tab.opened = true;
+ vm = createComponent({
+ tab,
+ });
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.state.entries[tab.path] = tab;
+ vm.$store.dispatch('setFileActive', tab.path);
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
new file mode 100644
index 00000000000..ceb0416aff8
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+import repoTabs from '~/ide/components/repo_tabs.vue';
+import createComponent from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('RepoTabs', () => {
+ const openedFiles = [file('open1'), file('open2')];
+ const RepoTabs = Vue.extend(repoTabs);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a list of tabs', done => {
+ vm = createComponent(RepoTabs, {
+ files: openedFiles,
+ viewer: 'editor',
+ hasChanges: false,
+ });
+ openedFiles[0].active = true;
+
+ vm.$nextTick(() => {
+ const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
+
+ expect(tabs.length).toEqual(2);
+ expect(tabs[0].classList.contains('active')).toEqual(true);
+ expect(tabs[1].classList.contains('active')).toEqual(false);
+
+ done();
+ });
+ });
+
+ describe('updated', () => {
+ it('sets showShadow as true when scroll width is larger than width', done => {
+ const el = document.createElement('div');
+ el.innerHTML = '<div id="test-app"></div>';
+ document.body.appendChild(el);
+
+ const style = document.createElement('style');
+ style.innerText = `
+ .multi-file-tabs {
+ width: 100px;
+ }
+
+ .multi-file-tabs .list-unstyled {
+ display: flex;
+ overflow-x: auto;
+ }
+ `;
+ document.head.appendChild(style);
+
+ vm = createComponent(
+ RepoTabs,
+ {
+ files: [],
+ viewer: 'editor',
+ hasChanges: false,
+ },
+ '#test-app',
+ );
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.showShadow).toEqual(false);
+
+ vm.files = openedFiles;
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.showShadow).toEqual(true);
+
+ style.remove();
+ el.remove();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js
new file mode 100644
index 00000000000..98db6defc7a
--- /dev/null
+++ b/spec/javascripts/ide/helpers.js
@@ -0,0 +1,22 @@
+import { decorateData } from '~/ide/stores/utils';
+import state from '~/ide/stores/state';
+import commitState from '~/ide/stores/modules/commit/state';
+
+export const resetStore = store => {
+ const newState = {
+ ...state(),
+ commit: commitState(),
+ };
+ store.replaceState(newState);
+};
+
+export const file = (name = 'name', id = name, type = '') =>
+ decorateData({
+ id,
+ type,
+ icon: 'icon',
+ url: 'url',
+ name,
+ path: name,
+ lastCommit: {},
+ });
diff --git a/spec/javascripts/ide/lib/common/disposable_spec.js b/spec/javascripts/ide/lib/common/disposable_spec.js
new file mode 100644
index 00000000000..af12ca15369
--- /dev/null
+++ b/spec/javascripts/ide/lib/common/disposable_spec.js
@@ -0,0 +1,44 @@
+import Disposable from '~/ide/lib/common/disposable';
+
+describe('Multi-file editor library disposable class', () => {
+ let instance;
+ let disposableClass;
+
+ beforeEach(() => {
+ instance = new Disposable();
+
+ disposableClass = {
+ dispose: jasmine.createSpy('dispose'),
+ };
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('add', () => {
+ it('adds disposable classes', () => {
+ instance.add(disposableClass);
+
+ expect(instance.disposers.size).toBe(1);
+ });
+ });
+
+ describe('dispose', () => {
+ beforeEach(() => {
+ instance.add(disposableClass);
+ });
+
+ it('calls dispose on all cached disposers', () => {
+ instance.dispose();
+
+ expect(disposableClass.dispose).toHaveBeenCalled();
+ });
+
+ it('clears cached disposers', () => {
+ instance.dispose();
+
+ expect(instance.disposers.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js
new file mode 100644
index 00000000000..4381f6fcfd0
--- /dev/null
+++ b/spec/javascripts/ide/lib/common/model_manager_spec.js
@@ -0,0 +1,129 @@
+/* global monaco */
+import eventHub from '~/ide/eventhub';
+import monacoLoader from '~/ide/monaco_loader';
+import ModelManager from '~/ide/lib/common/model_manager';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model manager', () => {
+ let instance;
+
+ beforeEach(done => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = new ModelManager(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('addModel', () => {
+ it('caches model', () => {
+ instance.addModel(file());
+
+ expect(instance.models.size).toBe(1);
+ });
+
+ it('caches model by file path', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.models.keys().next().value).toBe('path-name');
+ });
+
+ it('adds model into disposable', () => {
+ spyOn(instance.disposable, 'add').and.callThrough();
+
+ instance.addModel(file());
+
+ expect(instance.disposable.add).toHaveBeenCalled();
+ });
+
+ it('returns cached model', () => {
+ spyOn(instance.models, 'get').and.callThrough();
+
+ instance.addModel(file());
+ instance.addModel(file());
+
+ expect(instance.models.get).toHaveBeenCalled();
+ });
+
+ it('adds eventHub listener', () => {
+ const f = file();
+ spyOn(eventHub, '$on').and.callThrough();
+
+ instance.addModel(f);
+
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${f.path}`,
+ jasmine.anything(),
+ );
+ });
+ });
+
+ describe('hasCachedModel', () => {
+ it('returns false when no models exist', () => {
+ expect(instance.hasCachedModel('path')).toBeFalsy();
+ });
+
+ it('returns true when model exists', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.hasCachedModel('path-name')).toBeTruthy();
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns cached model', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.getModel('path-name')).not.toBeNull();
+ });
+ });
+
+ describe('removeCachedModel', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file();
+
+ instance.addModel(f);
+ });
+
+ it('clears cached model', () => {
+ instance.removeCachedModel(f);
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('removes eventHub listener', () => {
+ spyOn(eventHub, '$off').and.callThrough();
+
+ instance.removeCachedModel(f);
+
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${f.path}`,
+ jasmine.anything(),
+ );
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached models', () => {
+ instance.addModel(file());
+
+ instance.dispose();
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('calls disposable dispose', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
new file mode 100644
index 00000000000..adc6a93c06b
--- /dev/null
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -0,0 +1,113 @@
+/* global monaco */
+import eventHub from '~/ide/eventhub';
+import monacoLoader from '~/ide/monaco_loader';
+import Model from '~/ide/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model', () => {
+ let model;
+
+ beforeEach(done => {
+ spyOn(eventHub, '$on').and.callThrough();
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ });
+
+ it('creates original model & new model', () => {
+ expect(model.originalModel).not.toBeNull();
+ expect(model.model).not.toBeNull();
+ });
+
+ it('adds eventHub listener', () => {
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${model.file.path}`,
+ jasmine.anything(),
+ );
+ });
+
+ describe('path', () => {
+ it('returns file path', () => {
+ expect(model.path).toBe('path');
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns model', () => {
+ expect(model.getModel()).toBe(model.model);
+ });
+ });
+
+ describe('getOriginalModel', () => {
+ it('returns original model', () => {
+ expect(model.getOriginalModel()).toBe(model.originalModel);
+ });
+ });
+
+ describe('setValue', () => {
+ it('updates models value', () => {
+ model.setValue('testing 123');
+
+ expect(model.getModel().getValue()).toBe('testing 123');
+ });
+ });
+
+ describe('onChange', () => {
+ it('caches event by path', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+ expect(model.events.keys().next().value).toBe('path');
+ });
+
+ it('calls callback on change', done => {
+ const spy = jasmine.createSpy();
+ model.onChange(spy);
+
+ model.getModel().setValue('123');
+
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalledWith(model, jasmine.anything());
+ done();
+ });
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(model.disposable, 'dispose').and.callThrough();
+
+ model.dispose();
+
+ expect(model.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('clears events', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+
+ model.dispose();
+
+ expect(model.events.size).toBe(0);
+ });
+
+ it('removes eventHub listener', () => {
+ spyOn(eventHub, '$off').and.callThrough();
+
+ model.dispose();
+
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${model.file.path}`,
+ jasmine.anything(),
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
new file mode 100644
index 00000000000..092170d086a
--- /dev/null
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -0,0 +1,139 @@
+/* global monaco */
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import Model from '~/ide/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library decorations controller', () => {
+ let editorInstance;
+ let controller;
+ let model;
+
+ beforeEach(done => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ controller = new DecorationsController(editorInstance);
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ editorInstance.dispose();
+ controller.dispose();
+ });
+
+ describe('getAllDecorationsForModel', () => {
+ it('returns empty array when no decorations exist for model', () => {
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations).toEqual([]);
+ });
+
+ it('returns decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations[0]).toEqual({ decoration: 'decorationValue' });
+ });
+ });
+
+ describe('addDecorations', () => {
+ it('caches decorations in a new map', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('does not create new cache model', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue2' },
+ ]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ expect(controller.decorations.size).toBe(1);
+ expect(controller.decorations.keys().next().value).toBe('path');
+ });
+
+ it('calls decorate method', () => {
+ spyOn(controller, 'decorate');
+
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ expect(controller.decorate).toHaveBeenCalled();
+ });
+ });
+
+ describe('decorate', () => {
+ it('sets decorations on editor instance', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations');
+
+ controller.decorate(model);
+
+ expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith(
+ [],
+ [],
+ );
+ });
+
+ it('caches decorations', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.keys().next().value).toBe('path');
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached decorations', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ controller.dispose();
+
+ expect(controller.decorations.size).toBe(0);
+ });
+
+ it('clears cached editorDecorations', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ controller.dispose();
+
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
new file mode 100644
index 00000000000..c8f3e9f4830
--- /dev/null
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -0,0 +1,196 @@
+/* global monaco */
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import ModelManager from '~/ide/lib/common/model_manager';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import DirtyDiffController, {
+ getDiffChangeType,
+ getDecorator,
+} from '~/ide/lib/diff/controller';
+import { computeDiff } from '~/ide/lib/diff/diff';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library dirty diff controller', () => {
+ let editorInstance;
+ let controller;
+ let modelManager;
+ let decorationsController;
+ let model;
+
+ beforeEach(done => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ modelManager = new ModelManager(monaco);
+ decorationsController = new DecorationsController(editorInstance);
+
+ model = modelManager.addModel(file('path'));
+
+ controller = new DirtyDiffController(modelManager, decorationsController);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ controller.dispose();
+ model.dispose();
+ decorationsController.dispose();
+ editorInstance.dispose();
+ });
+
+ describe('getDiffChangeType', () => {
+ ['added', 'removed', 'modified'].forEach(type => {
+ it(`returns ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDiffChangeType(change)).toBe(type);
+ });
+ });
+ });
+
+ describe('getDecorator', () => {
+ ['added', 'removed', 'modified'].forEach(type => {
+ it(`returns with linesDecorationsClassName for ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDecorator(change).options.linesDecorationsClassName).toBe(
+ `dirty-diff dirty-diff-${type}`,
+ );
+ });
+
+ it('returns with line numbers', () => {
+ const change = {
+ lineNumber: 1,
+ endLineNumber: 2,
+ [type]: true,
+ };
+
+ const range = getDecorator(change).range;
+
+ expect(range.startLineNumber).toBe(1);
+ expect(range.endLineNumber).toBe(2);
+ expect(range.startColumn).toBe(1);
+ expect(range.endColumn).toBe(1);
+ });
+ });
+ });
+
+ describe('attachModel', () => {
+ it('adds change event callback', () => {
+ spyOn(model, 'onChange');
+
+ controller.attachModel(model);
+
+ expect(model.onChange).toHaveBeenCalled();
+ });
+
+ it('calls throttledComputeDiff on change', () => {
+ spyOn(controller, 'throttledComputeDiff');
+
+ controller.attachModel(model);
+
+ model.getModel().setValue('123');
+
+ expect(controller.throttledComputeDiff).toHaveBeenCalled();
+ });
+ });
+
+ describe('computeDiff', () => {
+ it('posts to worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'postMessage');
+
+ controller.computeDiff(model);
+
+ expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({
+ path: model.path,
+ originalContent: '',
+ newContent: '',
+ });
+ });
+ });
+
+ describe('reDecorate', () => {
+ it('calls decorations controller decorate', () => {
+ spyOn(controller.decorationsController, 'decorate');
+
+ controller.reDecorate(model);
+
+ expect(controller.decorationsController.decorate).toHaveBeenCalledWith(
+ model,
+ );
+ });
+ });
+
+ describe('decorate', () => {
+ it('adds decorations into decorations controller', () => {
+ spyOn(controller.decorationsController, 'addDecorations');
+
+ controller.decorate({ data: { changes: [], path: 'path' } });
+
+ expect(
+ controller.decorationsController.addDecorations,
+ ).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything());
+ });
+
+ it('adds decorations into editor', () => {
+ const spy = spyOn(
+ controller.decorationsController.editor.instance,
+ 'deltaDecorations',
+ );
+
+ controller.decorate({
+ data: { changes: computeDiff('123', '1234'), path: 'path' },
+ });
+
+ expect(spy).toHaveBeenCalledWith(
+ [],
+ [
+ {
+ range: new monaco.Range(1, 1, 1, 1),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
+ },
+ },
+ ],
+ );
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(controller.disposable, 'dispose').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('terminates worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled();
+ });
+
+ it('removes worker event listener', () => {
+ spyOn(
+ controller.dirtyDiffWorker,
+ 'removeEventListener',
+ ).and.callThrough();
+
+ controller.dispose();
+
+ expect(
+ controller.dirtyDiffWorker.removeEventListener,
+ ).toHaveBeenCalledWith('message', jasmine.anything());
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/diff/diff_spec.js b/spec/javascripts/ide/lib/diff/diff_spec.js
new file mode 100644
index 00000000000..57f3ac3d365
--- /dev/null
+++ b/spec/javascripts/ide/lib/diff/diff_spec.js
@@ -0,0 +1,80 @@
+import { computeDiff } from '~/ide/lib/diff/diff';
+
+describe('Multi-file editor library diff calculator', () => {
+ describe('computeDiff', () => {
+ it('returns empty array if no changes', () => {
+ const diff = computeDiff('123', '123');
+
+ expect(diff).toEqual([]);
+ });
+
+ describe('modified', () => {
+ it('', () => {
+ const diff = computeDiff('123', '1234')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ describe('added', () => {
+ it('', () => {
+ const diff = computeDiff('123', '123\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(3);
+ });
+ });
+
+ describe('removed', () => {
+ it('', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeTruthy();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeTruthy();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ it('includes line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.lineNumber).toBe(1);
+ });
+
+ it('includes end line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.endLineNumber).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/editor_options_spec.js b/spec/javascripts/ide/lib/editor_options_spec.js
new file mode 100644
index 00000000000..d149a883166
--- /dev/null
+++ b/spec/javascripts/ide/lib/editor_options_spec.js
@@ -0,0 +1,11 @@
+import editorOptions from '~/ide/lib/editor_options';
+
+describe('Multi-file editor library editor options', () => {
+ it('returns an array', () => {
+ expect(editorOptions).toEqual(jasmine.any(Array));
+ });
+
+ it('contains readOnly option', () => {
+ expect(editorOptions[0].readOnly).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
new file mode 100644
index 00000000000..2ccd87de1a7
--- /dev/null
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -0,0 +1,193 @@
+/* global monaco */
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import { file } from '../helpers';
+
+describe('Multi-file editor library', () => {
+ let instance;
+ let el;
+ let holder;
+
+ beforeEach(done => {
+ el = document.createElement('div');
+ holder = document.createElement('div');
+ el.appendChild(holder);
+
+ document.body.appendChild(el);
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = editor.create(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+
+ el.remove();
+ });
+
+ it('creates instance of editor', () => {
+ expect(editor.editorInstance).not.toBeNull();
+ });
+
+ it('creates instance returns cached instance', () => {
+ expect(editor.create(monaco)).toEqual(instance);
+ });
+
+ describe('createInstance', () => {
+ it('creates editor instance', () => {
+ spyOn(instance.monaco.editor, 'create').and.callThrough();
+
+ instance.createInstance(holder);
+
+ expect(instance.monaco.editor.create).toHaveBeenCalled();
+ });
+
+ it('creates dirty diff controller', () => {
+ instance.createInstance(holder);
+
+ expect(instance.dirtyDiffController).not.toBeNull();
+ });
+
+ it('creates model manager', () => {
+ instance.createInstance(holder);
+
+ expect(instance.modelManager).not.toBeNull();
+ });
+ });
+
+ describe('createDiffInstance', () => {
+ it('creates editor instance', () => {
+ spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough();
+
+ instance.createDiffInstance(holder);
+
+ expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith(holder, {
+ model: null,
+ contextmenu: true,
+ minimap: {
+ enabled: false,
+ },
+ readOnly: true,
+ scrollBeyondLastLine: false,
+ quickSuggestions: false,
+ occurrencesHighlight: false,
+ renderLineHighlight: 'none',
+ hideCursorInOverviewRuler: true,
+ wordWrap: 'bounded',
+ });
+ });
+ });
+
+ describe('createModel', () => {
+ it('calls model manager addModel', () => {
+ spyOn(instance.modelManager, 'addModel');
+
+ instance.createModel('FILE');
+
+ expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
+ });
+ });
+
+ describe('attachModel', () => {
+ let model;
+
+ beforeEach(() => {
+ instance.createInstance(document.createElement('div'));
+
+ model = instance.createModel(file());
+ });
+
+ it('sets the current model on the instance', () => {
+ instance.attachModel(model);
+
+ expect(instance.currentModel).toBe(model);
+ });
+
+ it('attaches the model to the current instance', () => {
+ spyOn(instance.instance, 'setModel');
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
+ });
+
+ it('sets original & modified when diff editor', () => {
+ spyOn(instance.instance, 'getEditorType').and.returnValue('vs.editor.IDiffEditor');
+ spyOn(instance.instance, 'setModel');
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith({
+ original: model.getOriginalModel(),
+ modified: model.getModel(),
+ });
+ });
+
+ it('attaches the model to the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'attachModel');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
+ });
+
+ it('re-decorates with the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'reDecorate');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
+ });
+ });
+
+ describe('clearEditor', () => {
+ it('resets the editor model', () => {
+ instance.createInstance(document.createElement('div'));
+
+ spyOn(instance.instance, 'setModel');
+
+ instance.clearEditor();
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposble dispose method', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('resets instance', () => {
+ instance.createInstance(document.createElement('div'));
+
+ expect(instance.instance).not.toBeNull();
+
+ instance.dispose();
+
+ expect(instance.instance).toBeNull();
+ });
+
+ it('does not dispose modelManager', () => {
+ spyOn(instance.modelManager, 'dispose');
+
+ instance.dispose();
+
+ expect(instance.modelManager.dispose).not.toHaveBeenCalled();
+ });
+
+ it('does not dispose decorationsController', () => {
+ spyOn(instance.decorationsController, 'dispose');
+
+ instance.dispose();
+
+ expect(instance.decorationsController.dispose).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/monaco_loader_spec.js b/spec/javascripts/ide/monaco_loader_spec.js
new file mode 100644
index 00000000000..7ab315aa8c8
--- /dev/null
+++ b/spec/javascripts/ide/monaco_loader_spec.js
@@ -0,0 +1,15 @@
+import monacoContext from 'monaco-editor/dev/vs/loader';
+import monacoLoader from '~/ide/monaco_loader';
+
+describe('MonacoLoader', () => {
+ it('calls require.config and exports require', () => {
+ expect(monacoContext.require.getConfig()).toEqual(
+ jasmine.objectContaining({
+ paths: {
+ vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
+ },
+ }),
+ );
+ expect(monacoLoader).toBe(monacoContext.require);
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
new file mode 100644
index 00000000000..5b7c8365641
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -0,0 +1,421 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import router from '~/ide/ide_router';
+import eventHub from '~/ide/eventhub';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store file actions', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('closeFile', () => {
+ let localFile;
+
+ beforeEach(() => {
+ localFile = file('testFile');
+ localFile.active = true;
+ localFile.opened = true;
+ localFile.parentTreeUrl = 'parentTreeUrl';
+
+ store.state.openFiles.push(localFile);
+ store.state.entries[localFile.path] = localFile;
+ });
+
+ it('closes open files', done => {
+ store
+ .dispatch('closeFile', localFile.path)
+ .then(() => {
+ expect(localFile.opened).toBeFalsy();
+ expect(localFile.active).toBeFalsy();
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('closes file even if file has changes', done => {
+ store.state.changedFiles.push(localFile);
+
+ store
+ .dispatch('closeFile', localFile.path)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+ expect(store.state.changedFiles.length).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('closes file & opens next available file', done => {
+ const f = {
+ ...file('newOpenFile'),
+ url: '/newOpenFile',
+ };
+
+ store.state.openFiles.push(f);
+ store.state.entries[f.path] = f;
+
+ store
+ .dispatch('closeFile', localFile.path)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(`/project${f.url}`);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('setFileActive', () => {
+ let localFile;
+ let scrollToTabSpy;
+ let oldScrollToTab;
+
+ beforeEach(() => {
+ scrollToTabSpy = jasmine.createSpy('scrollToTab');
+ oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
+ store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
+
+ localFile = file('setThisActive');
+
+ store.state.entries[localFile.path] = localFile;
+ });
+
+ afterEach(() => {
+ store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
+ });
+
+ it('calls scrollToTab', done => {
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(scrollToTabSpy).toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the file active', done => {
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns early if file is already active', done => {
+ localFile.active = true;
+
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(scrollToTabSpy).not.toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets current active file to not active', done => {
+ const f = file('newActive');
+ store.state.entries[f.path] = f;
+ localFile.active = true;
+ store.state.openFiles.push(localFile);
+
+ store
+ .dispatch('setFileActive', f.path)
+ .then(() => {
+ expect(localFile.active).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('resets location.hash for line highlighting', done => {
+ location.hash = 'test';
+
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(location.hash).not.toBe('test');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('getFileData', () => {
+ let localFile;
+
+ beforeEach(() => {
+ spyOn(service, 'getFileData').and.returnValue(
+ Promise.resolve({
+ headers: {
+ 'page-title': 'testing getFileData',
+ },
+ json: () =>
+ Promise.resolve({
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ }),
+ }),
+ );
+
+ localFile = file(`newCreate-${Math.random()}`);
+ localFile.url = 'getFileDataURL';
+ store.state.entries[localFile.path] = localFile;
+ });
+
+ it('calls the service', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the file data', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(localFile.blamePath).toBe('blame_path');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets document title', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(document.title).toBe('testing getFileData');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the file as active', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds the file to open files', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(localFile.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('getRawFileData', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
+
+ tmpFile = file('tmpFile');
+ store.state.entries[tmpFile.path] = tmpFile;
+ });
+
+ it('calls getRawFileData service method', done => {
+ store
+ .dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('updates file raw data', done => {
+ store
+ .dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(tmpFile.raw).toBe('raw');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('changeFileContent', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ tmpFile = file('tmpFile');
+ store.state.entries[tmpFile.path] = tmpFile;
+ });
+
+ it('updates file content', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() => {
+ expect(tmpFile.content).toBe('content');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds file into changedFiles array', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds file once into changedFiles array', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() =>
+ store.dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content 123',
+ }),
+ )
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('removes file from changedFiles array if not changed', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() =>
+ store.dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: '',
+ }),
+ )
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardFileChanges', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ spyOn(eventHub, '$on');
+
+ tmpFile = file();
+ tmpFile.content = 'testing';
+
+ store.state.changedFiles.push(tmpFile);
+ store.state.entries[tmpFile.path] = tmpFile;
+ });
+
+ it('resets file content', done => {
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(tmpFile.content).not.toBe('testing');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('removes file from changedFiles array', done => {
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('closes temp file', done => {
+ tmpFile.tempFile = true;
+ tmpFile.opened = true;
+
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(tmpFile.opened).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not re-open a closed temp file', done => {
+ tmpFile.tempFile = true;
+
+ expect(tmpFile.opened).toBeFalsy();
+
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(tmpFile.opened).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
new file mode 100644
index 00000000000..381f038067b
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -0,0 +1,172 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store tree actions', () => {
+ let projectTree;
+
+ const basicCallParameters = {
+ endpoint: 'rootEndpoint',
+ projectId: 'abcproject',
+ branch: 'master',
+ branchId: 'master',
+ };
+
+ beforeEach(() => {
+ spyOn(router, 'push');
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: '',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('getFiles', () => {
+ beforeEach(() => {
+ spyOn(service, 'getFiles').and.returnValue(
+ Promise.resolve({
+ json: () =>
+ Promise.resolve([
+ 'file.txt',
+ 'folder/fileinfolder.js',
+ 'folder/subfolder/fileinsubfolder.js',
+ ]),
+ }),
+ );
+ });
+
+ it('calls service getFiles', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .then(() => {
+ expect(service.getFiles).toHaveBeenCalledWith('', 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds data into tree', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .then(() => {
+ projectTree = store.state.trees['abcproject/master'];
+ expect(projectTree.tree.length).toBe(2);
+ expect(projectTree.tree[0].type).toBe('tree');
+ expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
+ expect(projectTree.tree[1].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].name).toBe(
+ 'fileinsubfolder.js',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('toggleTreeOpen', () => {
+ let tree;
+
+ beforeEach(() => {
+ tree = file('testing', '1', 'tree');
+ store.state.entries[tree.path] = tree;
+ });
+
+ it('toggles the tree open', done => {
+ store
+ .dispatch('toggleTreeOpen', tree.path)
+ .then(() => {
+ expect(tree.opened).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('getLastCommitData', () => {
+ beforeEach(() => {
+ spyOn(service, 'getTreeLastCommit').and.returnValue(
+ Promise.resolve({
+ headers: {
+ 'more-logs-url': null,
+ },
+ json: () =>
+ Promise.resolve([
+ {
+ type: 'tree',
+ file_name: 'testing',
+ commit: {
+ message: 'commit message',
+ authored_date: '123',
+ },
+ },
+ ]),
+ }),
+ );
+
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ projectTree = store.state.trees['abcproject/mybranch'];
+ projectTree.tree.push(file('testing', '1', 'tree'));
+ projectTree.lastCommitPath = 'lastcommitpath';
+ });
+
+ it('calls service with lastCommitPath', done => {
+ store
+ .dispatch('getLastCommitData', projectTree)
+ .then(() => {
+ expect(service.getTreeLastCommit).toHaveBeenCalledWith(
+ 'lastcommitpath',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('updates trees last commit data', done => {
+ store
+ .dispatch('getLastCommitData', projectTree)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(projectTree.tree[0].lastCommit.message).toBe('commit message');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not update entry if not found', done => {
+ projectTree.tree[0].name = 'a';
+
+ store
+ .dispatch('getLastCommitData', projectTree)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(projectTree.tree[0].lastCommit.message).not.toBe(
+ 'commit message',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
new file mode 100644
index 00000000000..cec572f4507
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -0,0 +1,306 @@
+import * as urlUtils from '~/lib/utils/url_utility';
+import store from '~/ide/stores';
+import router from '~/ide/ide_router';
+import { resetStore, file } from '../helpers';
+
+describe('Multi-file store actions', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('redirectToUrl', () => {
+ it('calls visitUrl', done => {
+ spyOn(urlUtils, 'visitUrl');
+
+ store
+ .dispatch('redirectToUrl', 'test')
+ .then(() => {
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith('test');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('setInitialData', () => {
+ it('commits initial data', done => {
+ store
+ .dispatch('setInitialData', { canCommit: true })
+ .then(() => {
+ expect(store.state.canCommit).toBeTruthy();
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardAllChanges', () => {
+ beforeEach(() => {
+ const f = file('discardAll');
+ f.changed = true;
+
+ store.state.openFiles.push(f);
+ store.state.changedFiles.push(f);
+ store.state.entries[f.path] = f;
+ });
+
+ it('discards changes in file', done => {
+ store
+ .dispatch('discardAllChanges')
+ .then(() => {
+ expect(store.state.openFiles.changed).toBeFalsy();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('removes all files from changedFiles state', done => {
+ store
+ .dispatch('discardAllChanges')
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+ expect(store.state.openFiles.length).toBe(1);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('closeAllFiles', () => {
+ beforeEach(() => {
+ const f = file('closeAll');
+ store.state.openFiles.push(f);
+ store.state.openFiles[0].opened = true;
+ store.state.entries[f.path] = f;
+ });
+
+ it('closes all open files', done => {
+ store
+ .dispatch('closeAllFiles')
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('createTempEntry', () => {
+ beforeEach(() => {
+ document.body.innerHTML += '<div class="flash-container"></div>';
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'mybranch';
+
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ store.state.projects.abcproject = {
+ web_url: '',
+ };
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ describe('tree', () => {
+ it('creates temp tree', done => {
+ store
+ .dispatch('createTempEntry', {
+ branchId: store.state.currentBranchId,
+ name: 'test',
+ type: 'tree',
+ })
+ .then(() => {
+ const entry = store.state.entries.test;
+
+ expect(entry).not.toBeNull();
+ expect(entry.type).toBe('tree');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('creates new folder inside another tree', done => {
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ path: 'testing',
+ tree: [],
+ };
+
+ store.state.entries[tree.path] = tree;
+
+ store
+ .dispatch('createTempEntry', {
+ branchId: store.state.currentBranchId,
+ name: 'testing/test',
+ type: 'tree',
+ })
+ .then(() => {
+ expect(tree.tree[0].tempFile).toBeTruthy();
+ expect(tree.tree[0].name).toBe('test');
+ expect(tree.tree[0].type).toBe('tree');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not create new tree if already exists', done => {
+ const tree = {
+ type: 'tree',
+ path: 'testing',
+ tempFile: false,
+ tree: [],
+ };
+
+ store.state.entries[tree.path] = tree;
+
+ store
+ .dispatch('createTempEntry', {
+ branchId: store.state.currentBranchId,
+ name: 'testing',
+ type: 'tree',
+ })
+ .then(() => {
+ expect(store.state.entries[tree.path].tempFile).toEqual(false);
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('blob', () => {
+ it('creates temp file', done => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ expect(f.tempFile).toBeTruthy();
+ expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(
+ 1,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds tmp file to open files', done => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(f.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds tmp file to changed files', done => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ expect(store.state.changedFiles.length).toBe(1);
+ expect(store.state.changedFiles[0].name).toBe(f.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets tmp file as active', done => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ expect(f.active).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('creates flash message if file already exists', done => {
+ const f = file('test', '1', 'blob');
+ store.state.trees['abcproject/mybranch'].tree = [f];
+ store.state.entries[f.path] = f;
+
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(() => {
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('popHistoryState', () => {});
+
+ describe('scrollToTab', () => {
+ it('focuses the current active element', done => {
+ document.body.innerHTML +=
+ '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>';
+ const el = document.querySelector('.repo-tab');
+ spyOn(el, 'focus');
+
+ store
+ .dispatch('scrollToTab')
+ .then(() => {
+ setTimeout(() => {
+ expect(el.focus).toHaveBeenCalled();
+
+ document.getElementById('tabs').remove();
+
+ done();
+ });
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateViewer', () => {
+ it('updates viewer state', done => {
+ store
+ .dispatch('updateViewer', 'diff')
+ .then(() => {
+ expect(store.state.viewer).toBe('diff');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
new file mode 100644
index 00000000000..a613f3a21cc
--- /dev/null
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -0,0 +1,55 @@
+import * as getters from '~/ide/stores/getters';
+import state from '~/ide/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store getters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('activeFile', () => {
+ it('returns the current active file', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+ localState.openFiles[1].active = true;
+
+ expect(getters.activeFile(localState).name).toBe('active');
+ });
+
+ it('returns undefined if no active files are found', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+
+ expect(getters.activeFile(localState)).toBeNull();
+ });
+ });
+
+ describe('modifiedFiles', () => {
+ it('returns a list of modified files', () => {
+ localState.openFiles.push(file());
+ localState.changedFiles.push(file('changed'));
+ localState.changedFiles[0].changed = true;
+
+ const modifiedFiles = getters.modifiedFiles(localState);
+
+ expect(modifiedFiles.length).toBe(1);
+ expect(modifiedFiles[0].name).toBe('changed');
+ });
+ });
+
+ describe('addedFiles', () => {
+ it('returns a list of added files', () => {
+ localState.openFiles.push(file());
+ localState.changedFiles.push(file('added'));
+ localState.changedFiles[0].changed = true;
+ localState.changedFiles[0].tempFile = true;
+
+ const modifiedFiles = getters.addedFiles(localState);
+
+ expect(modifiedFiles.length).toBe(1);
+ expect(modifiedFiles[0].name).toBe('added');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
new file mode 100644
index 00000000000..90ded940227
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -0,0 +1,505 @@
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import router from '~/ide/ide_router';
+import * as urlUtils from '~/lib/utils/url_utility';
+import eventHub from '~/ide/eventhub';
+import * as consts from '~/ide/stores/modules/commit/constants';
+import { resetStore, file } from 'spec/ide/helpers';
+
+describe('IDE commit module actions', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('updateCommitMessage', () => {
+ it('updates store with new commit message', done => {
+ store
+ .dispatch('commit/updateCommitMessage', 'testing')
+ .then(() => {
+ expect(store.state.commit.commitMessage).toBe('testing');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardDraft', () => {
+ it('resets commit message to blank', done => {
+ store.state.commit.commitMessage = 'testing';
+
+ store
+ .dispatch('commit/discardDraft')
+ .then(() => {
+ expect(store.state.commit.commitMessage).not.toBe('testing');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateCommitAction', () => {
+ it('updates store with new commit action', done => {
+ store
+ .dispatch('commit/updateCommitAction', '1')
+ .then(() => {
+ expect(store.state.commit.commitAction).toBe('1');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateBranchName', () => {
+ it('updates store with new branch name', done => {
+ store
+ .dispatch('commit/updateBranchName', 'branch-name')
+ .then(() => {
+ expect(store.state.commit.newBranchName).toBe('branch-name');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('setLastCommitMessage', () => {
+ beforeEach(() => {
+ Object.assign(store.state, {
+ currentProjectId: 'abcproject',
+ projects: {
+ abcproject: {
+ web_url: 'http://testing',
+ },
+ },
+ });
+ });
+
+ it('updates commit message with short_id', done => {
+ store
+ .dispatch('commit/setLastCommitMessage', { short_id: '123' })
+ .then(() => {
+ expect(store.state.lastCommitMsg).toContain(
+ 'Your changes have been committed. Commit <a href="http://testing/commit/123" class="commit-sha">123</a>',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates commit message with stats', done => {
+ store
+ .dispatch('commit/setLastCommitMessage', {
+ short_id: '123',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ })
+ .then(() => {
+ expect(store.state.lastCommitMsg).toBe(
+ 'Your changes have been committed. Commit <a href="http://testing/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('checkCommitStatus', () => {
+ beforeEach(() => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+ });
+
+ it('calls service', done => {
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '123' },
+ },
+ }),
+ );
+
+ store
+ .dispatch('commit/checkCommitStatus')
+ .then(() => {
+ expect(service.getBranchData).toHaveBeenCalledWith(
+ 'abcproject',
+ 'master',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns true if current ref does not equal returned ID', done => {
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '123' },
+ },
+ }),
+ );
+
+ store
+ .dispatch('commit/checkCommitStatus')
+ .then(val => {
+ expect(val).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns false if current ref equals returned ID', done => {
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '1' },
+ },
+ }),
+ );
+
+ store
+ .dispatch('commit/checkCommitStatus')
+ .then(val => {
+ expect(val).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateFilesAfterCommit', () => {
+ const data = {
+ id: '123',
+ message: 'testing commit message',
+ committed_date: '123',
+ committer_name: 'root',
+ };
+ const branch = 'master';
+ let f;
+
+ beforeEach(() => {
+ spyOn(eventHub, '$emit');
+
+ f = file('changedFile');
+ Object.assign(f, {
+ active: true,
+ changed: true,
+ content: 'file content',
+ });
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: 'web_url',
+ branches: {
+ master: {
+ workingReference: '',
+ },
+ },
+ };
+ store.state.changedFiles.push(f, {
+ ...file('changedFile2'),
+ changed: true,
+ });
+ store.state.openFiles = store.state.changedFiles;
+
+ store.state.changedFiles.forEach(changedFile => {
+ store.state.entries[changedFile.path] = changedFile;
+ });
+ });
+
+ it('updates stores working reference', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(
+ store.state.projects.abcproject.branches.master.workingReference,
+ ).toBe(data.id);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('resets all files changed status', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ store.state.openFiles.forEach(entry => {
+ expect(entry.changed).toBeFalsy();
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('removes all changed files', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('sets files commit data', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(f.lastCommit.message).toBe(data.message);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates raw content for changed file', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(f.raw).toBe(f.content);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('emits changed event for file', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ `editor.update.model.content.${f.path}`,
+ f.content,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('pushes route to new branch if commitAction is new branch', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(
+ `/project/abcproject/blob/master/${f.path}`,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('resets stores commit actions', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(store.state.commit.commitAction).not.toBe(
+ consts.COMMIT_TO_NEW_BRANCH,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('commitChanges', () => {
+ beforeEach(() => {
+ spyOn(urlUtils, 'visitUrl');
+
+ document.body.innerHTML += '<div class="flash-container"></div>';
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: 'webUrl',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+ store.state.changedFiles.push(file('changed'));
+ store.state.changedFiles[0].active = true;
+ store.state.openFiles = store.state.changedFiles;
+
+ store.state.openFiles.forEach(f => {
+ store.state.entries[f.path] = f;
+ });
+
+ store.state.commit.commitAction = '2';
+ store.state.commit.commitMessage = 'testing 123';
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(
+ Promise.resolve({
+ data: {
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ },
+ }),
+ );
+ });
+
+ it('calls service', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(service.commit).toHaveBeenCalledWith('abcproject', {
+ branch: jasmine.anything(),
+ commit_message: 'testing 123',
+ actions: [
+ {
+ action: 'update',
+ file_path: jasmine.anything(),
+ content: jasmine.anything(),
+ encoding: jasmine.anything(),
+ },
+ ],
+ start_branch: 'master',
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('pushes router to new route', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(
+ `/project/${store.state.currentProjectId}/blob/${
+ store.getters['commit/newBranchName']
+ }/changed`,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets last Commit Msg', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.lastCommitMsg).toBe(
+ 'Your changes have been committed. Commit <a href="webUrl/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds commit data to changed files', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.openFiles[0].lastCommit.message).toBe(
+ 'test message',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('redirects to new merge request page', done => {
+ spyOn(eventHub, '$on');
+
+ store.state.commit.commitAction = '3';
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith(
+ `webUrl/merge_requests/new?merge_request[source_branch]=${
+ store.getters['commit/newBranchName']
+ }&merge_request[target_branch]=master`,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('failed', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(
+ Promise.resolve({
+ data: {
+ message: 'failed message',
+ },
+ }),
+ );
+ });
+
+ it('shows failed message', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ const alert = document.querySelector('.flash-container');
+
+ expect(alert.textContent.trim()).toBe('failed message');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
new file mode 100644
index 00000000000..e396284ec2c
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -0,0 +1,128 @@
+import commitState from '~/ide/stores/modules/commit/state';
+import * as consts from '~/ide/stores/modules/commit/constants';
+import * as getters from '~/ide/stores/modules/commit/getters';
+
+describe('IDE commit module getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = commitState();
+ });
+
+ describe('discardDraftButtonDisabled', () => {
+ it('returns true when commitMessage is empty', () => {
+ expect(getters.discardDraftButtonDisabled(state)).toBeTruthy();
+ });
+
+ it('returns false when commitMessage is not empty & loading is false', () => {
+ state.commitMessage = 'test';
+ state.submitCommitLoading = false;
+
+ expect(getters.discardDraftButtonDisabled(state)).toBeFalsy();
+ });
+
+ it('returns true when commitMessage is not empty & loading is true', () => {
+ state.commitMessage = 'test';
+ state.submitCommitLoading = true;
+
+ expect(getters.discardDraftButtonDisabled(state)).toBeTruthy();
+ });
+ });
+
+ describe('commitButtonDisabled', () => {
+ const localGetters = {
+ discardDraftButtonDisabled: false,
+ };
+ const rootState = {
+ changedFiles: ['a'],
+ };
+
+ it('returns false when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeFalsy();
+ });
+
+ it('returns true when discardDraftButtonDisabled is false & changedFiles is empty', () => {
+ rootState.changedFiles.length = 0;
+
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeTruthy();
+ });
+
+ it('returns true when discardDraftButtonDisabled is true', () => {
+ localGetters.discardDraftButtonDisabled = true;
+
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeTruthy();
+ });
+
+ it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
+ localGetters.discardDraftButtonDisabled = false;
+ rootState.changedFiles.length = 0;
+
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('newBranchName', () => {
+ it('includes username, currentBranchId, patch & random number', () => {
+ gon.current_username = 'username';
+
+ const branch = getters.newBranchName(state, null, {
+ currentBranchId: 'testing',
+ });
+
+ expect(branch).toMatch(/username-testing-patch-\d{5}$/);
+ });
+ });
+
+ describe('branchName', () => {
+ const rootState = {
+ currentBranchId: 'master',
+ };
+ const localGetters = {
+ newBranchName: 'newBranchName',
+ };
+
+ beforeEach(() => {
+ Object.assign(state, {
+ newBranchName: 'state-newBranchName',
+ });
+ });
+
+ it('defualts to currentBranchId', () => {
+ expect(getters.branchName(state, null, rootState)).toBe('master');
+ });
+
+ ['COMMIT_TO_NEW_BRANCH', 'COMMIT_TO_NEW_BRANCH_MR'].forEach(type => {
+ describe(type, () => {
+ beforeEach(() => {
+ Object.assign(state, {
+ commitAction: consts[type],
+ });
+ });
+
+ it('uses newBranchName when not empty', () => {
+ expect(getters.branchName(state, localGetters, rootState)).toBe(
+ 'state-newBranchName',
+ );
+ });
+
+ it('uses getters newBranchName when state newBranchName is empty', () => {
+ Object.assign(state, {
+ newBranchName: '',
+ });
+
+ expect(getters.branchName(state, localGetters, rootState)).toBe(
+ 'newBranchName',
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/commit/mutations_spec.js b/spec/javascripts/ide/stores/modules/commit/mutations_spec.js
new file mode 100644
index 00000000000..5de7a281d34
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/commit/mutations_spec.js
@@ -0,0 +1,42 @@
+import commitState from '~/ide/stores/modules/commit/state';
+import mutations from '~/ide/stores/modules/commit/mutations';
+
+describe('IDE commit module mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = commitState();
+ });
+
+ describe('UPDATE_COMMIT_MESSAGE', () => {
+ it('updates commitMessage', () => {
+ mutations.UPDATE_COMMIT_MESSAGE(state, 'testing');
+
+ expect(state.commitMessage).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_COMMIT_ACTION', () => {
+ it('updates commitAction', () => {
+ mutations.UPDATE_COMMIT_ACTION(state, 'testing');
+
+ expect(state.commitAction).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_NEW_BRANCH_NAME', () => {
+ it('updates newBranchName', () => {
+ mutations.UPDATE_NEW_BRANCH_NAME(state, 'testing');
+
+ expect(state.newBranchName).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_LOADING', () => {
+ it('updates submitCommitLoading', () => {
+ mutations.UPDATE_LOADING(state, true);
+
+ expect(state.submitCommitLoading).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js
new file mode 100644
index 00000000000..a7167537ef2
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/branch_spec.js
@@ -0,0 +1,18 @@
+import mutations from '~/ide/stores/mutations/branch';
+import state from '~/ide/stores/state';
+
+describe('Multi-file store branch mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('SET_CURRENT_BRANCH', () => {
+ it('sets currentBranch', () => {
+ mutations.SET_CURRENT_BRANCH(localState, 'master');
+
+ expect(localState.currentBranchId).toBe('master');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
new file mode 100644
index 00000000000..131380248e8
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -0,0 +1,157 @@
+import mutations from '~/ide/stores/mutations/file';
+import state from '~/ide/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store file mutations', () => {
+ let localState;
+ let localFile;
+
+ beforeEach(() => {
+ localState = state();
+ localFile = file();
+
+ localState.entries[localFile.path] = localFile;
+ });
+
+ describe('SET_FILE_ACTIVE', () => {
+ it('sets the file active', () => {
+ mutations.SET_FILE_ACTIVE(localState, {
+ path: localFile.path,
+ active: true,
+ });
+
+ expect(localFile.active).toBeTruthy();
+ });
+ });
+
+ describe('TOGGLE_FILE_OPEN', () => {
+ beforeEach(() => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
+ });
+
+ it('adds into opened files', () => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localState.openFiles.length).toBe(1);
+ });
+
+ it('removes from opened files', () => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
+
+ expect(localFile.opened).toBeFalsy();
+ expect(localState.openFiles.length).toBe(0);
+ });
+ });
+
+ describe('SET_FILE_DATA', () => {
+ it('sets extra file data', () => {
+ mutations.SET_FILE_DATA(localState, {
+ data: {
+ blame_path: 'blame',
+ commits_path: 'commits',
+ permalink: 'permalink',
+ raw_path: 'raw',
+ binary: true,
+ render_error: 'render_error',
+ },
+ file: localFile,
+ });
+
+ expect(localFile.blamePath).toBe('blame');
+ expect(localFile.commitsPath).toBe('commits');
+ expect(localFile.permalink).toBe('permalink');
+ expect(localFile.rawPath).toBe('raw');
+ expect(localFile.binary).toBeTruthy();
+ expect(localFile.renderError).toBe('render_error');
+ });
+ });
+
+ describe('SET_FILE_RAW_DATA', () => {
+ it('sets raw data', () => {
+ mutations.SET_FILE_RAW_DATA(localState, {
+ file: localFile,
+ raw: 'testing',
+ });
+
+ expect(localFile.raw).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_FILE_CONTENT', () => {
+ beforeEach(() => {
+ localFile.raw = 'test';
+ });
+
+ it('sets content', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ path: localFile.path,
+ content: 'test',
+ });
+
+ expect(localFile.content).toBe('test');
+ });
+
+ it('sets changed if content does not match raw', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ path: localFile.path,
+ content: 'testing',
+ });
+
+ expect(localFile.content).toBe('testing');
+ expect(localFile.changed).toBeTruthy();
+ });
+
+ it('sets changed if file is a temp file', () => {
+ localFile.tempFile = true;
+
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ path: localFile.path,
+ content: '',
+ });
+
+ expect(localFile.changed).toBeTruthy();
+ });
+ });
+
+ describe('DISCARD_FILE_CHANGES', () => {
+ beforeEach(() => {
+ localFile.content = 'test';
+ localFile.changed = true;
+ });
+
+ it('resets content and changed', () => {
+ mutations.DISCARD_FILE_CHANGES(localState, localFile.path);
+
+ expect(localFile.content).toBe('');
+ expect(localFile.changed).toBeFalsy();
+ });
+ });
+
+ describe('ADD_FILE_TO_CHANGED', () => {
+ it('adds file into changed files array', () => {
+ mutations.ADD_FILE_TO_CHANGED(localState, localFile.path);
+
+ expect(localState.changedFiles.length).toBe(1);
+ });
+ });
+
+ describe('REMOVE_FILE_FROM_CHANGED', () => {
+ it('removes files from changed files array', () => {
+ localState.changedFiles.push(localFile);
+
+ mutations.REMOVE_FILE_FROM_CHANGED(localState, localFile.path);
+
+ expect(localState.changedFiles.length).toBe(0);
+ });
+ });
+
+ describe('TOGGLE_FILE_CHANGED', () => {
+ it('updates file changed status', () => {
+ mutations.TOGGLE_FILE_CHANGED(localState, {
+ file: localFile,
+ changed: true,
+ });
+
+ expect(localFile.changed).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js
new file mode 100644
index 00000000000..e6c085eaff6
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/tree_spec.js
@@ -0,0 +1,69 @@
+import mutations from '~/ide/stores/mutations/tree';
+import state from '~/ide/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store tree mutations', () => {
+ let localState;
+ let localTree;
+
+ beforeEach(() => {
+ localState = state();
+ localTree = file();
+
+ localState.entries[localTree.path] = localTree;
+ });
+
+ describe('TOGGLE_TREE_OPEN', () => {
+ it('toggles tree open', () => {
+ mutations.TOGGLE_TREE_OPEN(localState, localTree.path);
+
+ expect(localTree.opened).toBeTruthy();
+
+ mutations.TOGGLE_TREE_OPEN(localState, localTree.path);
+
+ expect(localTree.opened).toBeFalsy();
+ });
+ });
+
+ describe('SET_DIRECTORY_DATA', () => {
+ const data = [
+ {
+ name: 'tree',
+ },
+ {
+ name: 'submodule',
+ },
+ {
+ name: 'blob',
+ },
+ ];
+
+ it('adds directory data', () => {
+ localState.trees['project/master'] = {
+ tree: [],
+ };
+
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ treePath: 'project/master',
+ });
+
+ const tree = localState.trees['project/master'];
+
+ expect(tree.tree.length).toBe(3);
+ expect(tree.tree[0].name).toBe('tree');
+ expect(tree.tree[1].name).toBe('submodule');
+ expect(tree.tree[2].name).toBe('blob');
+ });
+ });
+
+ describe('REMOVE_ALL_CHANGES_FILES', () => {
+ it('removes all files from changedFiles state', () => {
+ localState.changedFiles.push(file('REMOVE_ALL_CHANGES_FILES'));
+
+ mutations.REMOVE_ALL_CHANGES_FILES(localState);
+
+ expect(localState.changedFiles.length).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
new file mode 100644
index 00000000000..38162a470ad
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -0,0 +1,79 @@
+import mutations from '~/ide/stores/mutations';
+import state from '~/ide/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store mutations', () => {
+ let localState;
+ let entry;
+
+ beforeEach(() => {
+ localState = state();
+ entry = file();
+
+ localState.entries[entry.path] = entry;
+ });
+
+ describe('SET_INITIAL_DATA', () => {
+ it('sets all initial data', () => {
+ mutations.SET_INITIAL_DATA(localState, {
+ test: 'test',
+ });
+
+ expect(localState.test).toBe('test');
+ });
+ });
+
+ describe('TOGGLE_LOADING', () => {
+ it('toggles loading of entry', () => {
+ mutations.TOGGLE_LOADING(localState, { entry });
+
+ expect(entry.loading).toBeTruthy();
+
+ mutations.TOGGLE_LOADING(localState, { entry });
+
+ expect(entry.loading).toBeFalsy();
+ });
+
+ it('toggles loading of entry and sets specific value', () => {
+ mutations.TOGGLE_LOADING(localState, { entry });
+
+ expect(entry.loading).toBeTruthy();
+
+ mutations.TOGGLE_LOADING(localState, { entry, forceValue: true });
+
+ expect(entry.loading).toBeTruthy();
+ });
+ });
+
+ describe('SET_LEFT_PANEL_COLLAPSED', () => {
+ it('sets left panel collapsed', () => {
+ mutations.SET_LEFT_PANEL_COLLAPSED(localState, true);
+
+ expect(localState.leftPanelCollapsed).toBeTruthy();
+
+ mutations.SET_LEFT_PANEL_COLLAPSED(localState, false);
+
+ expect(localState.leftPanelCollapsed).toBeFalsy();
+ });
+ });
+
+ describe('SET_RIGHT_PANEL_COLLAPSED', () => {
+ it('sets right panel collapsed', () => {
+ mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true);
+
+ expect(localState.rightPanelCollapsed).toBeTruthy();
+
+ mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false);
+
+ expect(localState.rightPanelCollapsed).toBeFalsy();
+ });
+ });
+
+ describe('UPDATE_VIEWER', () => {
+ it('sets viewer state', () => {
+ mutations.UPDATE_VIEWER(localState, 'diff');
+
+ expect(localState.viewer).toBe('diff');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
new file mode 100644
index 00000000000..f38ac6dd82f
--- /dev/null
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -0,0 +1,66 @@
+import * as utils from '~/ide/stores/utils';
+
+describe('Multi-file store utils', () => {
+ describe('setPageTitle', () => {
+ it('sets the document page title', () => {
+ utils.setPageTitle('test');
+
+ expect(document.title).toBe('test');
+ });
+ });
+
+ describe('findIndexOfFile', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = [
+ {
+ path: '1',
+ },
+ {
+ path: '2',
+ },
+ ];
+ });
+
+ it('finds in the index of an entry by path', () => {
+ const index = utils.findIndexOfFile(localState, {
+ path: '2',
+ });
+
+ expect(index).toBe(1);
+ });
+ });
+
+ describe('findEntry', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = {
+ tree: [
+ {
+ type: 'tree',
+ name: 'test',
+ },
+ {
+ type: 'blob',
+ name: 'file',
+ },
+ ],
+ };
+ });
+
+ it('returns an entry found by name', () => {
+ const foundEntry = utils.findEntry(localState.tree, 'tree', 'test');
+
+ expect(foundEntry.type).toBe('tree');
+ expect(foundEntry.name).toBe('test');
+ });
+
+ it('returns undefined when no entry found', () => {
+ const foundEntry = utils.findEntry(localState.tree, 'blob', 'test');
+
+ expect(foundEntry).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 584db6c6632..d5a87b5ce20 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -1,8 +1,7 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import '~/render_math';
-import '~/render_gfm';
+import '~/behaviors/markdown/render_gfm';
import * as urlUtils from '~/lib/utils/url_utility';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js
index a95a7e2a5be..ca0e7c395a0 100644
--- a/spec/javascripts/lib/utils/text_markdown_spec.js
+++ b/spec/javascripts/lib/utils/text_markdown_spec.js
@@ -1,4 +1,4 @@
-import textUtils from '~/lib/utils/text_markdown';
+import { insertMarkdownText } from '~/lib/utils/text_markdown';
describe('init markdown', () => {
let textArea;
@@ -21,7 +21,7 @@ describe('init markdown', () => {
textArea.selectionStart = 0;
textArea.selectionEnd = 0;
- textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+ insertMarkdownText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
@@ -32,7 +32,7 @@ describe('init markdown', () => {
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
- textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+ insertMarkdownText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}\n* `);
});
@@ -43,7 +43,7 @@ describe('init markdown', () => {
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
- textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+ insertMarkdownText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
@@ -54,7 +54,7 @@ describe('init markdown', () => {
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
- textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+ insertMarkdownText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js
index eb644e698da..dc9dc4d4249 100644
--- a/spec/javascripts/merge_request_notes_spec.js
+++ b/spec/javascripts/merge_request_notes_spec.js
@@ -3,8 +3,7 @@ import _ from 'underscore';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
-import '~/render_gfm';
-import '~/render_math';
+import '~/behaviors/markdown/render_gfm';
import Notes from '~/notes';
const upArrowKeyCode = 38;
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 29b355307ef..eba6dcf47c5 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -18,6 +18,7 @@ describe('Dashboard', () => {
deploymentEndpoint: null,
emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
emptyLoadingSvgPath: '/path/to/loading.svg',
+ emptyNoDataSvgPath: '/path/to/no-data.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
};
diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js
index df3198dd3e2..b4c5f4baa78 100644
--- a/spec/javascripts/monitoring/dashboard_state_spec.js
+++ b/spec/javascripts/monitoring/dashboard_state_spec.js
@@ -2,13 +2,22 @@ import Vue from 'vue';
import EmptyState from '~/monitoring/components/empty_state.vue';
import { statePaths } from './mock_data';
-const createComponent = (propsData) => {
+function createComponent(props) {
const Component = Vue.extend(EmptyState);
return new Component({
- propsData,
+ propsData: {
+ ...props,
+ settingsPath: statePaths.settingsPath,
+ clustersPath: statePaths.clustersPath,
+ documentationPath: statePaths.documentationPath,
+ emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
+ emptyLoadingSvgPath: '/path/to/loading.svg',
+ emptyNoDataSvgPath: '/path/to/no-data.svg',
+ emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
+ },
}).$mount();
-};
+}
function getTextFromNode(component, selector) {
return component.$el.querySelector(selector).firstChild.nodeValue.trim();
@@ -19,11 +28,6 @@ describe('EmptyState', () => {
it('currentState', () => {
const component = createComponent({
selectedState: 'gettingStarted',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.currentState).toBe(component.states.gettingStarted);
@@ -32,11 +36,6 @@ describe('EmptyState', () => {
it('showButtonDescription returns a description with a link for the unableToConnect state', () => {
const component = createComponent({
selectedState: 'unableToConnect',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.showButtonDescription).toEqual(true);
@@ -45,11 +44,6 @@ describe('EmptyState', () => {
it('showButtonDescription returns the description without a link for any other state', () => {
const component = createComponent({
selectedState: 'loading',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.showButtonDescription).toEqual(false);
@@ -59,12 +53,6 @@ describe('EmptyState', () => {
it('should show the gettingStarted state', () => {
const component = createComponent({
selectedState: 'gettingStarted',
- settingsPath: statePaths.settingsPath,
- clustersPath: statePaths.clustersPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.$el.querySelector('svg')).toBeDefined();
@@ -76,11 +64,6 @@ describe('EmptyState', () => {
it('should show the loading state', () => {
const component = createComponent({
selectedState: 'loading',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.$el.querySelector('svg')).toBeDefined();
@@ -92,11 +75,6 @@ describe('EmptyState', () => {
it('should show the unableToConnect state', () => {
const component = createComponent({
selectedState: 'unableToConnect',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.$el.querySelector('svg')).toBeDefined();
diff --git a/spec/javascripts/notes/components/diff_file_header_spec.js b/spec/javascripts/notes/components/diff_file_header_spec.js
index aed30a087a6..ef6d513444a 100644
--- a/spec/javascripts/notes/components/diff_file_header_spec.js
+++ b/spec/javascripts/notes/components/diff_file_header_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import DiffFileHeader from '~/notes/components/diff_file_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const discussionFixture = 'merge_requests/diff_discussion.json';
diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js
index 7f1f4bf0bcd..f4ec7132dbd 100644
--- a/spec/javascripts/notes/components/diff_with_note_spec.js
+++ b/spec/javascripts/notes/components/diff_with_note_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import DiffWithNote from '~/notes/components/diff_with_note.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const discussionFixture = 'merge_requests/diff_discussion.json';
const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index ac39418c3e6..0e792eee5e9 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -3,7 +3,7 @@ import _ from 'underscore';
import Vue from 'vue';
import notesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service';
-import '~/render_gfm';
+import '~/behaviors/markdown/render_gfm';
import * as mockData from '../mock_data';
const vueMatchers = {
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index ba0a70bed17..ec56ab0e2f0 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -7,7 +7,7 @@ import * as urlUtils from '~/lib/utils/url_utility';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
-import '~/render_gfm';
+import '~/behaviors/markdown/render_gfm';
import Notes from '~/notes';
import timeoutPromise from './helpers/set_timeout_promise_helper';
@@ -16,15 +16,15 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
window.gl = window.gl || {};
gl.utils = gl.utils || {};
- const htmlEscape = (comment) => {
- const escapedString = comment.replace(/["&'<>]/g, (a) => {
+ const htmlEscape = comment => {
+ const escapedString = comment.replace(/["&'<>]/g, a => {
const escapedToken = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
- '`': '&#x60;'
+ '`': '&#x60;',
}[a];
return escapedToken;
@@ -39,7 +39,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw';
preloadFixtures(commentsTemplate);
- beforeEach(function () {
+ beforeEach(function() {
loadFixtures(commentsTemplate);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
@@ -51,6 +51,17 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$('body').removeAttr('data-page');
});
+ describe('addBinding', () => {
+ it('calls postComment when comment button is clicked', () => {
+ spyOn(Notes.prototype, 'postComment');
+ this.notes = new Notes('', []);
+
+ $('.js-comment-button').click();
+
+ expect(Notes.prototype.postComment).toHaveBeenCalled();
+ });
+ });
+
describe('task lists', function() {
let mock;
@@ -58,7 +69,13 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
spyOn(axios, 'patch').and.callThrough();
mock = new MockAdapter(axios);
- mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {});
+ mock
+ .onPatch(
+ `${
+ gl.TEST_HOST
+ }/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
+ )
+ .reply(200, {});
$('.js-comment-button').on('click', function(e) {
e.preventDefault();
@@ -73,18 +90,27 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('modifies the Markdown field', function() {
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
- $('input[type=checkbox]').attr('checked', true)[1].dispatchEvent(changeEvent);
+ $('input[type=checkbox]')
+ .attr('checked', true)[1]
+ .dispatchEvent(changeEvent);
- expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item');
+ expect($('.js-task-list-field.original-task-list').val()).toBe(
+ '- [x] Task List Item',
+ );
});
it('submits an ajax request on tasklist:changed', function(done) {
$('.js-task-list-container').trigger('tasklist:changed');
setTimeout(() => {
- expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, {
- note: { note: '' },
- });
+ expect(axios.patch).toHaveBeenCalledWith(
+ `${
+ gl.TEST_HOST
+ }/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
+ {
+ note: { note: '' },
+ },
+ );
done();
});
});
@@ -100,10 +126,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
spyOn(this.notes, 'renderNote').and.stub();
$(textarea).data('autosave', {
- reset: function() {}
+ reset: function() {},
});
- $('.js-comment-button').on('click', (e) => {
+ $('.js-comment-button').on('click', e => {
const $form = $(this);
e.preventDefault();
this.notes.addNote($form);
@@ -149,7 +175,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
<div class="note-text">${sampleComment}</div>
</li>`,
note: sampleComment,
- valid: true
+ valid: true,
};
$form = $('form.js-main-target-form');
$notesContainer = $('ul.main-notes-list');
@@ -163,7 +189,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
mock.restore();
});
- it('updates note and resets edit form', (done) => {
+ it('updates note and resets edit form', done => {
spyOn(this.notes, 'revertNoteEditForm');
spyOn(this.notes, 'setupNewNote');
@@ -175,7 +201,9 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
updatedNote.note = 'bar';
this.notes.updateNote(updatedNote, $targetNote);
- expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
+ expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith(
+ $targetNote,
+ );
expect(this.notes.setupNewNote).toHaveBeenCalled();
done();
@@ -231,17 +259,14 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
note: 'heya',
html: '<div>heya</div>',
};
- $notesList = jasmine.createSpyObj('$notesList', [
- 'find',
- 'append',
- ]);
+ $notesList = jasmine.createSpyObj('$notesList', ['find', 'append']);
notes = jasmine.createSpyObj('notes', [
'setupNewNote',
'refresh',
'collapseLongCommitList',
'updateNotesCount',
- 'putConflictEditWarningInPlace'
+ 'putConflictEditWarningInPlace',
]);
notes.taskList = jasmine.createSpyObj('tasklist', ['init']);
notes.note_ids = [];
@@ -258,7 +283,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
Notes.isNewNote.and.returnValue(true);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(
+ note.html,
+ $notesList,
+ );
});
});
@@ -273,7 +301,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note);
+ expect(Notes.animateUpdateNote).toHaveBeenCalledWith(
+ note.html,
+ $note,
+ );
expect(notes.setupNewNote).toHaveBeenCalledWith($newNote);
});
@@ -301,7 +332,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$notesList.find.and.returnValue($note);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note);
+ expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(
+ note,
+ $note,
+ );
});
});
});
@@ -311,11 +345,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should consider same note text as the same', () => {
const result = Notes.isUpdatedNote(
{
- note: 'initial'
+ note: 'initial',
},
$(`<div>
<div class="original-note-content">initial</div>
- </div>`)
+ </div>`),
);
expect(result).toEqual(false);
@@ -324,11 +358,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should consider same note with trailing newline as the same', () => {
const result = Notes.isUpdatedNote(
{
- note: 'initial\n'
+ note: 'initial\n',
},
$(`<div>
<div class="original-note-content">initial\n</div>
- </div>`)
+ </div>`),
);
expect(result).toEqual(false);
@@ -337,11 +371,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should consider different notes as different', () => {
const result = Notes.isUpdatedNote(
{
- note: 'foo'
+ note: 'foo',
},
$(`<div>
<div class="original-note-content">bar</div>
- </div>`)
+ </div>`),
);
expect(result).toEqual(true);
@@ -397,7 +431,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should call Notes.animateAppendNote', () => {
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $('.main-notes-list'));
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(
+ note.discussion_html,
+ $('.main-notes-list'),
+ );
});
it('should append to row selected with line_code', () => {
@@ -428,7 +465,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('should call Notes.animateAppendNote', () => {
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer);
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(
+ note.html,
+ discussionContainer,
+ );
});
});
});
@@ -461,9 +501,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
beforeEach(() => {
noteHTML = '<div></div>';
- $note = jasmine.createSpyObj('$note', [
- 'replaceWith'
- ]);
+ $note = jasmine.createSpyObj('$note', ['replaceWith']);
$updatedNote = Notes.animateUpdateNote(noteHTML, $note);
});
@@ -501,7 +539,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
<div class="note-text">${sampleComment}</div>
</li>`,
note: sampleComment,
- valid: true
+ valid: true,
};
let $form;
let $notesContainer;
@@ -534,10 +572,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
mockNotesPost();
$('.js-comment-button').click();
- expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true);
+ expect($notesContainer.find('.note.being-posted').length > 0).toEqual(
+ true,
+ );
});
- it('should remove placeholder note when new comment is done posting', (done) => {
+ it('should remove placeholder note when new comment is done posting', done => {
mockNotesPost();
$('.js-comment-button').click();
@@ -549,19 +589,44 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
});
- it('should show actual note element when new comment is done posting', (done) => {
+ describe('postComment', () => {
+ it('disables the submit button', done => {
+ const $submitButton = $form.find('.js-comment-submit-button');
+ expect($submitButton).not.toBeDisabled();
+ const dummyEvent = {
+ preventDefault() {},
+ target: $submitButton,
+ };
+ mock.onPost(NOTES_POST_PATH).replyOnce(() => {
+ expect($submitButton).toBeDisabled();
+ return [200, note];
+ });
+
+ this.notes
+ .postComment(dummyEvent)
+ .then(() => {
+ expect($submitButton).not.toBeDisabled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ it('should show actual note element when new comment is done posting', done => {
mockNotesPost();
$('.js-comment-button').click();
setTimeout(() => {
- expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true);
+ expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(
+ true,
+ );
done();
});
});
- it('should reset Form when new comment is done posting', (done) => {
+ it('should reset Form when new comment is done posting', done => {
mockNotesPost();
$('.js-comment-button').click();
@@ -573,19 +638,24 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
});
- it('should show flash error message when new comment failed to be posted', (done) => {
+ it('should show flash error message when new comment failed to be posted', done => {
mockNotesPostError();
$('.js-comment-button').click();
setTimeout(() => {
- expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true);
+ expect(
+ $notesContainer
+ .parent()
+ .find('.flash-container .flash-text')
+ .is(':visible'),
+ ).toEqual(true);
done();
});
});
- it('should show flash error message when comment failed to be updated', (done) => {
+ it('should show flash error message when comment failed to be updated', done => {
mockNotesPost();
$('.js-comment-button').click();
@@ -606,7 +676,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
.then(() => {
const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
- expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original
+ expect(
+ $updatedNoteEl
+ .find('.note-text')
+ .text()
+ .trim(),
+ ).toEqual(sampleComment); // See if comment reverted back to original
expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown
done();
@@ -620,12 +695,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const note = {
commands_changes: {
assignee_id: 1,
- emoji_award: '100'
+ emoji_award: '100',
},
errors: {
- commands_only: ['Commands applied']
+ commands_only: ['Commands applied'],
},
- valid: false
+ valid: false,
};
let $form;
let $notesContainer;
@@ -640,12 +715,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
window.gon.current_user_fullname = 'Administrator';
gl.awardsHandler = {
addAwardToEmojiBar: () => {},
- scrollToAwards: () => {}
+ scrollToAwards: () => {},
};
gl.GfmAutoComplete = {
dataSources: {
- commands: '/root/test-project/autocomplete_sources/commands'
- }
+ commands: '/root/test-project/autocomplete_sources/commands',
+ },
};
$form = $('form.js-main-target-form');
$notesContainer = $('ul.main-notes-list');
@@ -656,14 +731,18 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
mock.restore();
});
- it('should remove slash command placeholder when comment with slash commands is done posting', (done) => {
+ it('should remove slash command placeholder when comment with slash commands is done posting', done => {
spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
$('.js-comment-button').click();
- expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
+ expect(
+ $notesContainer.find('.system-note.being-posted').length,
+ ).toEqual(1); // Placeholder shown
setTimeout(() => {
- expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
+ expect(
+ $notesContainer.find('.system-note.being-posted').length,
+ ).toEqual(0); // Placeholder removed
done();
});
});
@@ -678,7 +757,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
<div class="note-text">${sampleComment}</div>
</li>`,
note: sampleComment,
- valid: true
+ valid: true,
};
let $form;
let $notesContainer;
@@ -700,7 +779,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
mock.restore();
});
- it('should not render a script tag', (done) => {
+ it('should not render a script tag', done => {
$('.js-comment-button').click();
setTimeout(() => {
@@ -709,8 +788,15 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$noteEl.find('textarea.js-note-text').html(updatedComment);
$noteEl.find('.js-comment-save-button').click();
- const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container');
- expect($updatedNoteEl.find('.note-text').text().trim()).toEqual('');
+ const $updatedNoteEl = $notesContainer
+ .find(`#note_${note.id}`)
+ .find('.js-task-list-container');
+ expect(
+ $updatedNoteEl
+ .find('.note-text')
+ .text()
+ .trim(),
+ ).toEqual('');
done();
});
@@ -730,7 +816,9 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should return form metadata object from form reference', () => {
$form.find('textarea.js-note-text').val(sampleComment);
- const { formData, formContent, formAction } = this.notes.getFormData($form);
+ const { formData, formContent, formAction } = this.notes.getFormData(
+ $form,
+ );
expect(formData.indexOf(sampleComment) > -1).toBe(true);
expect(formContent).toEqual(sampleComment);
@@ -746,7 +834,9 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const { formContent } = this.notes.getFormData($form);
expect(_.escape).toHaveBeenCalledWith(sampleComment);
- expect(formContent).toEqual('&lt;script&gt;alert(&quot;Boom!&quot;);&lt;/script&gt;');
+ expect(formContent).toEqual(
+ '&lt;script&gt;alert(&quot;Boom!&quot;);&lt;/script&gt;',
+ );
});
});
@@ -756,7 +846,8 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('should return true when comment begins with a quick action', () => {
- const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
+ const sampleComment =
+ '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const hasQuickActions = this.notes.hasQuickActions(sampleComment);
expect(hasQuickActions).toBeTruthy();
@@ -780,7 +871,8 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('stripQuickActions', () => {
it('should strip quick actions from the comment which begins with a quick action', () => {
this.notes = new Notes();
- const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
+ const sampleComment =
+ '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('');
@@ -788,7 +880,8 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should strip quick actions from the comment but leaves plain comment if it is present', () => {
this.notes = new Notes();
- const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
+ const sampleComment =
+ '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('Merging this');
@@ -796,7 +889,8 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should NOT strip string that has slashes within', () => {
this.notes = new Notes();
- const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
+ const sampleComment =
+ 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe(sampleComment);
@@ -807,7 +901,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const availableQuickActions = [
{ name: 'close', description: 'Close this issue', params: [] },
{ name: 'title', description: 'Change title', params: [{}] },
- { name: 'estimate', description: 'Set time estimate', params: [{}] }
+ { name: 'estimate', description: 'Set time estimate', params: [{}] },
];
beforeEach(() => {
@@ -816,17 +910,29 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should return executing quick action description when note has single quick action', () => {
const sampleComment = '/close';
- expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe('Applying command to close this issue');
+ expect(
+ this.notes.getQuickActionDescription(
+ sampleComment,
+ availableQuickActions,
+ ),
+ ).toBe('Applying command to close this issue');
});
it('should return generic multiple quick action description when note has multiple quick actions', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe('Applying multiple commands');
+ expect(
+ this.notes.getQuickActionDescription(
+ sampleComment,
+ availableQuickActions,
+ ),
+ ).toBe('Applying multiple commands');
});
it('should return generic quick action description when available quick actions list is not populated', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(this.notes.getQuickActionDescription(sampleComment)).toBe('Applying command');
+ expect(this.notes.getQuickActionDescription(sampleComment)).toBe(
+ 'Applying command',
+ );
});
});
@@ -856,14 +962,35 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
expect($tempNote.attr('id')).toEqual(uniqueId);
expect($tempNote.hasClass('being-posted')).toBeTruthy();
expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
- $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() {
- expect($(this).attr('href')).toEqual(`/${currentUsername}`);
- });
- expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar);
- expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
- expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname);
- expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`);
- expect($tempNote.find('.note-body .note-text p').text().trim()).toEqual(sampleComment);
+ $tempNote
+ .find('.timeline-icon > a, .note-header-info > a')
+ .each(function() {
+ expect($(this).attr('href')).toEqual(`/${currentUsername}`);
+ });
+ expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(
+ currentUserAvatar,
+ );
+ expect(
+ $tempNote.find('.timeline-content').hasClass('discussion'),
+ ).toBeFalsy();
+ expect(
+ $tempNoteHeader
+ .find('.hidden-xs')
+ .text()
+ .trim(),
+ ).toEqual(currentUserFullname);
+ expect(
+ $tempNoteHeader
+ .find('.note-headline-light')
+ .text()
+ .trim(),
+ ).toEqual(`@${currentUsername}`);
+ expect(
+ $tempNote
+ .find('.note-body .note-text p')
+ .text()
+ .trim(),
+ ).toEqual(sampleComment);
});
it('should return constructed placeholder element for discussion note based on form contents', () => {
@@ -872,11 +999,13 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
uniqueId,
isDiscussionNote: true,
currentUsername,
- currentUserFullname
+ currentUserFullname,
});
expect($tempNote.prop('nodeName')).toEqual('LI');
- expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
+ expect(
+ $tempNote.find('.timeline-content').hasClass('discussion'),
+ ).toBeTruthy();
});
it('should return a escaped user name', () => {
@@ -890,7 +1019,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
currentUserAvatar,
});
const $tempNoteHeader = $tempNote.find('.note-header');
- expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual('Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;');
+ expect(
+ $tempNoteHeader
+ .find('.hidden-xs')
+ .text()
+ .trim(),
+ ).toEqual('Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;');
});
});
@@ -913,7 +1047,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
expect($tempNote.attr('id')).toEqual(uniqueId);
expect($tempNote.hasClass('being-posted')).toBeTruthy();
expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
- expect($tempNote.find('.timeline-content i').text().trim()).toEqual(sampleCommandDescription);
+ expect(
+ $tempNote
+ .find('.timeline-content i')
+ .text()
+ .trim(),
+ ).toEqual(sampleCommandDescription);
});
});
@@ -923,7 +1062,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('shows a flash message', () => {
- this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
+ this.notes.addFlash(
+ 'Error message',
+ FLASH_TYPE_ALERT,
+ this.notes.parentTimeline.get(0),
+ );
expect($('.flash-alert').is(':visible')).toBeTruthy();
});
@@ -936,7 +1079,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('hides visible flash message', () => {
- this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
+ this.notes.addFlash(
+ 'Error message 1',
+ FLASH_TYPE_ALERT,
+ this.notes.parentTimeline.get(0),
+ );
this.notes.clearFlash();
@@ -944,4 +1091,4 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
});
});
-}).call(window);
+}.call(window));
diff --git a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
index ba2e07f02f7..080158a8ee0 100644
--- a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
+++ b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import promoteLabelModal from '~/pages/projects/labels/components/promote_label_modal.vue';
import eventHub from '~/pages/projects/labels/event_hub';
import axios from '~/lib/utils/axios_utils';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Promote label modal', () => {
let vm;
diff --git a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
index bf044fe8fb5..22956929e7b 100644
--- a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
+++ b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
import axios from '~/lib/utils/axios_utils';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Promote milestone modal', () => {
let vm;
diff --git a/spec/javascripts/performance_bar/components/detailed_metric_spec.js b/spec/javascripts/performance_bar/components/detailed_metric_spec.js
new file mode 100644
index 00000000000..c4611dc7662
--- /dev/null
+++ b/spec/javascripts/performance_bar/components/detailed_metric_spec.js
@@ -0,0 +1,80 @@
+import Vue from 'vue';
+import detailedMetric from '~/performance_bar/components/detailed_metric.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('detailedMetric', () => {
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('when the current request has no details', () => {
+ beforeEach(() => {
+ vm = mountComponent(Vue.extend(detailedMetric), {
+ currentRequest: {},
+ metric: 'gitaly',
+ header: 'Gitaly calls',
+ details: 'details',
+ keys: ['feature', 'request'],
+ });
+ });
+
+ it('does not render the element', () => {
+ expect(vm.$el.innerHTML).toEqual(undefined);
+ });
+ });
+
+ describe('when the current request has details', () => {
+ const requestDetails = [
+ { duration: '100', feature: 'find_commit', request: 'abcdef' },
+ { duration: '23', feature: 'rebase_in_progress', request: '' },
+ ];
+
+ beforeEach(() => {
+ vm = mountComponent(Vue.extend(detailedMetric), {
+ currentRequest: {
+ details: {
+ gitaly: {
+ duration: '123ms',
+ calls: '456',
+ details: requestDetails,
+ },
+ },
+ },
+ metric: 'gitaly',
+ header: 'Gitaly calls',
+ details: 'details',
+ keys: ['feature', 'request'],
+ });
+ });
+
+ it('diplays details', () => {
+ expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456');
+ });
+
+ it('adds a modal with a table of the details', () => {
+ vm.$el
+ .querySelectorAll('.performance-bar-modal td strong')
+ .forEach((duration, index) => {
+ expect(duration.innerText).toContain(requestDetails[index].duration);
+ });
+
+ vm.$el
+ .querySelectorAll('.performance-bar-modal td:nth-child(2)')
+ .forEach((feature, index) => {
+ expect(feature.innerText).toContain(requestDetails[index].feature);
+ });
+
+ vm.$el
+ .querySelectorAll('.performance-bar-modal td:nth-child(3)')
+ .forEach((request, index) => {
+ expect(request.innerText).toContain(requestDetails[index].request);
+ });
+ });
+
+ it('displays the metric name', () => {
+ expect(vm.$el.innerText).toContain('gitaly');
+ });
+ });
+});
diff --git a/spec/javascripts/performance_bar/components/performance_bar_app_spec.js b/spec/javascripts/performance_bar/components/performance_bar_app_spec.js
new file mode 100644
index 00000000000..9ab9ab1c9f4
--- /dev/null
+++ b/spec/javascripts/performance_bar/components/performance_bar_app_spec.js
@@ -0,0 +1,88 @@
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import performanceBarApp from '~/performance_bar/components/performance_bar_app.vue';
+import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
+import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import MockAdapter from 'axios-mock-adapter';
+
+describe('performance bar', () => {
+ let mock;
+ let vm;
+
+ beforeEach(() => {
+ const store = new PerformanceBarStore();
+
+ mock = new MockAdapter(axios);
+
+ mock.onGet('/-/peek/results').reply(
+ 200,
+ {
+ data: {
+ gc: {
+ invokes: 0,
+ invoke_time: '0.00',
+ use_size: 0,
+ total_size: 0,
+ total_object: 0,
+ gc_time: '0.00',
+ },
+ host: { hostname: 'web-01' },
+ },
+ },
+ {},
+ );
+
+ vm = mountComponent(Vue.extend(performanceBarApp), {
+ store,
+ env: 'development',
+ requestId: '123',
+ peekUrl: '/-/peek/results',
+ profileUrl: '?lineprofiler=true',
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ mock.restore();
+ });
+
+ it('sets the class to match the environment', () => {
+ expect(vm.$el.getAttribute('class')).toContain('development');
+ });
+
+ describe('loadRequestDetails', () => {
+ beforeEach(() => {
+ spyOn(vm.store, 'addRequest').and.callThrough();
+ });
+
+ it('does nothing if the request cannot be tracked', () => {
+ spyOn(vm.store, 'canTrackRequest').and.callFake(() => false);
+
+ vm.loadRequestDetails('123', 'https://gitlab.com/');
+
+ expect(vm.store.addRequest).not.toHaveBeenCalled();
+ });
+
+ it('adds the request immediately', () => {
+ vm.loadRequestDetails('123', 'https://gitlab.com/');
+
+ expect(vm.store.addRequest).toHaveBeenCalledWith(
+ '123',
+ 'https://gitlab.com/',
+ );
+ });
+
+ it('makes an HTTP request for the request details', () => {
+ spyOn(PerformanceBarService, 'fetchRequestDetails').and.callThrough();
+
+ vm.loadRequestDetails('456', 'https://gitlab.com/');
+
+ expect(PerformanceBarService.fetchRequestDetails).toHaveBeenCalledWith(
+ '/-/peek/results',
+ '456',
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/performance_bar/components/request_selector_spec.js b/spec/javascripts/performance_bar/components/request_selector_spec.js
new file mode 100644
index 00000000000..6108a29f8c4
--- /dev/null
+++ b/spec/javascripts/performance_bar/components/request_selector_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import requestSelector from '~/performance_bar/components/request_selector.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('request selector', () => {
+ const requests = [
+ { id: '123', url: 'https://gitlab.com/' },
+ {
+ id: '456',
+ url: 'https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1',
+ },
+ {
+ id: '789',
+ url:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1.json?serializer=widget',
+ },
+ ];
+
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Vue.extend(requestSelector), {
+ requests,
+ currentRequest: requests[1],
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ function optionText(requestId) {
+ return vm.$el.querySelector(`[value='${requestId}']`).innerText.trim();
+ }
+
+ it('displays the last component of the path', () => {
+ expect(optionText(requests[2].id)).toEqual('1.json?serializer=widget');
+ });
+
+ it('keeps the last two components of the path when the last component is numeric', () => {
+ expect(optionText(requests[1].id)).toEqual('merge_requests/1');
+ });
+
+ it('ignores trailing slashes', () => {
+ expect(optionText(requests[0].id)).toEqual('gitlab.com');
+ });
+});
diff --git a/spec/javascripts/performance_bar/components/simple_metric_spec.js b/spec/javascripts/performance_bar/components/simple_metric_spec.js
new file mode 100644
index 00000000000..98b843e9711
--- /dev/null
+++ b/spec/javascripts/performance_bar/components/simple_metric_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import simpleMetric from '~/performance_bar/components/simple_metric.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('simpleMetric', () => {
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('when the current request has no details', () => {
+ beforeEach(() => {
+ vm = mountComponent(Vue.extend(simpleMetric), {
+ currentRequest: {},
+ metric: 'gitaly',
+ });
+ });
+
+ it('does not display details', () => {
+ expect(vm.$el.innerText).not.toContain('/');
+ });
+
+ it('displays the metric name', () => {
+ expect(vm.$el.innerText).toContain('gitaly');
+ });
+ });
+
+ describe('when the current request has details', () => {
+ beforeEach(() => {
+ vm = mountComponent(Vue.extend(simpleMetric), {
+ currentRequest: {
+ details: { gitaly: { duration: '123ms', calls: '456' } },
+ },
+ metric: 'gitaly',
+ });
+ });
+
+ it('diplays details', () => {
+ expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456');
+ });
+
+ it('displays the metric name', () => {
+ expect(vm.$el.innerText).toContain('gitaly');
+ });
+ });
+});
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index faaf710cf6f..b0d714cbefb 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import initCopyAsGFM from '~/behaviors/copy_as_gfm';
+import initCopyAsGFM from '~/behaviors/markdown/copy_as_gfm';
import ShortcutsIssuable from '~/shortcuts_issuable';
initCopyAsGFM();
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index 31710551399..d9c03296857 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -23,9 +23,7 @@ const metricsMockData = {
memory_values: [
{
metric: {},
- values: [
- [1493716685, '4.30859375'],
- ],
+ values: [[1493716685, '4.30859375']],
},
],
},
@@ -53,7 +51,8 @@ const createComponent = () => {
const messages = {
loadingMetrics: 'Loading deployment statistics',
- hasMetrics: 'Memory usage unchanged from 0MB to 0MB',
+ hasMetrics:
+ '<a href="/root/acets-review-apps/environments/15/metrics"> Memory </a> usage is <b> unchanged </b> at 0MB',
loadFailed: 'Failed to load deployment statistics',
metricsUnavailable: 'Deployment statistics are not available currently',
};
@@ -92,26 +91,26 @@ describe('MemoryUsage', () => {
});
describe('computed', () => {
- describe('memoryChangeType', () => {
- it('should return "increased" if memoryFrom value is less than memoryTo value', () => {
+ describe('memoryChangeMessage', () => {
+ it('should contain "increased" if memoryFrom value is less than memoryTo value', () => {
vm.memoryFrom = 4.28;
vm.memoryTo = 9.13;
- expect(vm.memoryChangeType).toEqual('increased');
+ expect(vm.memoryChangeMessage.indexOf('increased')).not.toEqual('-1');
});
- it('should return "decreased" if memoryFrom value is less than memoryTo value', () => {
+ it('should contain "decreased" if memoryFrom value is less than memoryTo value', () => {
vm.memoryFrom = 9.13;
vm.memoryTo = 4.28;
- expect(vm.memoryChangeType).toEqual('decreased');
+ expect(vm.memoryChangeMessage.indexOf('decreased')).not.toEqual('-1');
});
- it('should return "unchanged" if memoryFrom value equal to memoryTo value', () => {
+ it('should contain "unchanged" if memoryFrom value equal to memoryTo value', () => {
vm.memoryFrom = 1;
vm.memoryTo = 1;
- expect(vm.memoryChangeType).toEqual('unchanged');
+ expect(vm.memoryChangeMessage.indexOf('unchanged')).not.toEqual('-1');
});
});
});
@@ -130,7 +129,13 @@ describe('MemoryUsage', () => {
describe('computeGraphData', () => {
it('should populate sparkline graph', () => {
vm.computeGraphData(metrics, deployment_time);
- const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm;
+ const {
+ hasMetrics,
+ memoryMetrics,
+ deploymentTime,
+ memoryFrom,
+ memoryTo,
+ } = vm;
expect(hasMetrics).toBeTruthy();
expect(memoryMetrics.length > 0).toBeTruthy();
@@ -141,20 +146,26 @@ describe('MemoryUsage', () => {
});
describe('loadMetrics', () => {
- const returnServicePromise = () => new Promise((resolve) => {
- resolve({
- data: metricsMockData,
+ const returnServicePromise = () =>
+ new Promise(resolve => {
+ resolve({
+ data: metricsMockData,
+ });
});
- });
- it('should load metrics data using MRWidgetService', (done) => {
- spyOn(MRWidgetService, 'fetchMetrics').and.returnValue(returnServicePromise(true));
+ it('should load metrics data using MRWidgetService', done => {
+ spyOn(MRWidgetService, 'fetchMetrics').and.returnValue(
+ returnServicePromise(true),
+ );
spyOn(vm, 'computeGraphData');
vm.loadMetrics();
setTimeout(() => {
expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
- expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
+ expect(vm.computeGraphData).toHaveBeenCalledWith(
+ metrics,
+ deployment_time,
+ );
done();
}, 333);
});
@@ -167,51 +178,67 @@ describe('MemoryUsage', () => {
expect(el.querySelector('.js-usage-info')).toBeDefined();
});
- it('should show loading metrics message while metrics are being loaded', (done) => {
+ it('should show loading metrics message while metrics are being loaded', done => {
vm.loadingMetrics = true;
vm.hasMetrics = false;
vm.loadFailed = false;
Vue.nextTick(() => {
- expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined();
- expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadingMetrics);
+ expect(
+ el.querySelector('.js-usage-info.usage-info-loading'),
+ ).toBeDefined();
+ expect(
+ el.querySelector('.js-usage-info .usage-info-load-spinner'),
+ ).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(
+ messages.loadingMetrics,
+ );
done();
});
});
- it('should show deployment memory usage when metrics are loaded', (done) => {
+ it('should show deployment memory usage when metrics are loaded', done => {
vm.loadingMetrics = false;
vm.hasMetrics = true;
vm.loadFailed = false;
Vue.nextTick(() => {
expect(el.querySelector('.memory-graph-container')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics);
+ expect(el.querySelector('.js-usage-info').innerText).toContain(
+ messages.hasMetrics,
+ );
done();
});
});
- it('should show failure message when metrics loading failed', (done) => {
+ it('should show failure message when metrics loading failed', done => {
vm.loadingMetrics = false;
vm.hasMetrics = false;
vm.loadFailed = true;
Vue.nextTick(() => {
- expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed);
+ expect(
+ el.querySelector('.js-usage-info.usage-info-failed'),
+ ).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(
+ messages.loadFailed,
+ );
done();
});
});
- it('should show metrics unavailable message when metrics loading failed', (done) => {
+ it('should show metrics unavailable message when metrics loading failed', done => {
vm.loadingMetrics = false;
vm.hasMetrics = false;
vm.loadFailed = false;
Vue.nextTick(() => {
- expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable);
+ expect(
+ el.querySelector('.js-usage-info.usage-info-unavailable'),
+ ).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(
+ messages.metricsUnavailable,
+ );
done();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index 5323523abc0..fcbd8169bc7 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import removeBreakLine from 'spec/helpers/vue_component_helper';
describe('MRWidgetConflicts', () => {
let Component;
@@ -78,8 +79,9 @@ describe('MRWidgetConflicts', () => {
});
it('should tell you to rebase locally', () => {
- expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('Fast-forward merge is not possible.');
- expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('To merge this request, first rebase locally');
+ expect(
+ removeBreakLine(vm.$el.textContent).trim(),
+ ).toContain('Fast-forward merge is not possible. To merge this request, first rebase locally.');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
index baacbc03fb1..894dbe3382f 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import removeBreakLine from 'spec/helpers/vue_component_helper';
describe('MRWidgetPipelineBlocked', () => {
let vm;
@@ -18,6 +19,8 @@ describe('MRWidgetPipelineBlocked', () => {
});
it('renders information text', () => {
- expect(vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ')).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
+ expect(
+ removeBreakLine(vm.$el.textContent).trim(),
+ ).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
index 4c67504b642..b02af94d03a 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -1,16 +1,25 @@
import Vue from 'vue';
-import shaMismatchComponent from '~/vue_merge_request_widget/components/states/mr_widget_sha_mismatch';
+import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import removeBreakLine from 'spec/helpers/vue_component_helper';
-describe('MRWidgetSHAMismatch', () => {
- describe('template', () => {
- const Component = Vue.extend(shaMismatchComponent);
- const vm = new Component({
- el: document.createElement('div'),
- });
- it('should have correct elements', () => {
- expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging');
- });
+describe('ShaMismatch', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ShaMismatch);
+ vm = mountComponent(Component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render information message', () => {
+ expect(vm.$el.querySelector('button').disabled).toEqual(true);
+
+ expect(
+ removeBreakLine(vm.$el.textContent).trim(),
+ ).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging');
});
});
diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/deprecated_modal_spec.js
index d01a94c25e5..59d4e549a91 100644
--- a/spec/javascripts/vue_shared/components/modal_spec.js
+++ b/spec/javascripts/vue_shared/components/deprecated_modal_spec.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import Vue from 'vue';
-import modal from '~/vue_shared/components/modal.vue';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-const modalComponent = Vue.extend(modal);
+const modalComponent = Vue.extend(DeprecatedModal);
-describe('Modal', () => {
+describe('DeprecatedModal', () => {
let vm;
afterEach(() => {
diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
index 818ef0af3c2..3e708f865c8 100644
--- a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('toolbar', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
index 8daaf018396..6fe95153204 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -3,9 +3,9 @@ import Vue from 'vue';
import LabelsSelect from '~/labels_select';
import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
-import { mockConfig, mockLabels } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockConfig, mockLabels } from './mock_data';
const createComponent = (config = mockConfig) => {
const Component = Vue.extend(baseComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
index ec63ac306d0..f25c70db125 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
-import { mockConfig, mockLabels } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockConfig, mockLabels } from './mock_data';
const componentConfig = Object.assign({}, mockConfig, {
fieldName: 'label_id[]',
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index 5cb4bb6fea6..ce559fe0335 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
-import { mockSuggestedColors } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockSuggestedColors } from './mock_data';
const createComponent = (headerTitle) => {
const Component = Vue.extend(dropdownCreateLabelComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
index 0f4fa716f8a..debeab25bd6 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
-import { mockConfig } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockConfig } from './mock_data';
const createComponent = (
labelsWebUrl = mockConfig.labelsWebUrl,
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
index 325fa47c957..cdf234bb0c4 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(dropdownHeaderComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
index 703b87498c7..88733922a59 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownHiddenInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue';
-import { mockLabels } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockLabels } from './mock_data';
const createComponent = (name = 'label_id[]', label = mockLabels[0]) => {
const Component = Vue.extend(dropdownHiddenInputComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
index 69e11d966c2..57608d957e7 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(dropdownSearchInputComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
index c3580933072..7c3d2711f65 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = (canEdit = true) => {
const Component = Vue.extend(dropdownTitleComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 93b42795bea..39040670a87 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
-import { mockLabels } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockLabels } from './mock_data';
const createComponent = (labels = mockLabels) => {
const Component = Vue.extend(dropdownValueCollapsedComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 66e0957b431..4397b00acfa 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
-import { mockConfig, mockLabels } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockConfig, mockLabels } from './mock_data';
const createComponent = (
labels = mockLabels,
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index 5100f5737c2..84688845fa5 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -278,6 +278,10 @@ describe Backup::Manager do
connection.directories.create(key: Gitlab.config.backup.upload.remote_directory)
end
+ after do
+ Fog.unmock!
+ end
+
context 'target path' do
it 'uses the tar filename by default' do
expect_any_instance_of(Fog::Collection).to receive(:create)
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index a9b5ed1112a..03573c304aa 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -33,7 +33,7 @@ describe Backup::Repository do
let(:timestamp) { Time.utc(2017, 3, 22) }
let(:temp_dirs) do
Gitlab.config.repositories.storages.map do |name, storage|
- File.join(storage['path'], '..', 'repositories.old.' + timestamp.to_i.to_s)
+ File.join(storage.legacy_disk_path, '..', 'repositories.old.' + timestamp.to_i.to_s)
end
end
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index b502daea418..a50329473ad 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -122,14 +122,10 @@ describe Banzai::Filter::AutolinkFilter do
end
it 'does not include trailing punctuation' do
- doc = filter("See #{link}.")
- expect(doc.at_css('a').text).to eq link
-
- doc = filter("See #{link}, ok?")
- expect(doc.at_css('a').text).to eq link
-
- doc = filter("See #{link}...")
- expect(doc.at_css('a').text).to eq link
+ ['.', ', ok?', '...', '?', '!', ': is that ok?'].each do |trailing_punctuation|
+ doc = filter("See #{link}#{trailing_punctuation}")
+ expect(doc.at_css('a').text).to eq link
+ end
end
it 'includes trailing punctuation when part of a balanced pair' do
@@ -171,6 +167,15 @@ describe Banzai::Filter::AutolinkFilter do
expect(actual).to eq(expected_complicated_link)
end
+ it 'does not double-encode HTML entities' do
+ encoded_link = "#{link}?foo=bar&amp;baz=quux"
+ expected_encoded_link = %Q{<a href="#{encoded_link}">#{encoded_link}</a>}
+ actual = unescape(filter(encoded_link).to_html)
+
+ expect(actual).to eq(Rinku.auto_link(encoded_link))
+ expect(actual).to eq(expected_encoded_link)
+ end
+
it 'does not include trailing HTML entities' do
doc = filter("See &lt;&lt;&lt;#{link}&gt;&gt;&gt;")
diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
index 17347768a49..a5373517ac8 100644
--- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
@@ -8,6 +8,7 @@ describe Banzai::Filter::IssuableStateFilter do
let(:context) { { current_user: user, issuable_state_filter_enabled: true } }
let(:closed_issue) { create_issue(:closed) }
let(:project) { create(:project, :public) }
+ let(:group) { create(:group) }
let(:other_project) { create(:project, :public) }
def create_link(text, data)
@@ -77,6 +78,13 @@ describe Banzai::Filter::IssuableStateFilter do
expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)} (closed)")
end
+ it 'handles references from group scopes' do
+ link = create_link(closed_issue.to_reference(other_project), issue: closed_issue.id, reference_type: 'issue')
+ doc = filter(link, context.merge(project: nil, group: group))
+
+ expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)} (closed)")
+ end
+
it 'skips cross project references if the user cannot read cross project' do
expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
link = create_link(closed_issue.to_reference(other_project), issue: closed_issue.id, reference_type: 'issue')
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 2a0e19ae796..e1782cff81a 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -48,7 +48,7 @@ module Gitlab
},
'images' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
- output: "<img src=\"https://localhost.com/image.png\" alt=\"Alt text\">"
+ output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt='Alt text\" onerror=\"alert(7)'></span></p>\n</div>"
},
'pre' => {
input: '```mypre"><script>alert(3)</script>',
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index f969f9e8e38..18cef8ec996 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -315,13 +315,19 @@ describe Gitlab::Auth do
it "tries to autheticate with db before ldap" do
expect(Gitlab::Auth::LDAP::Authentication).not_to receive(:login)
- gl_auth.find_with_user_password(username, password)
+ expect(gl_auth.find_with_user_password(username, password)).to eq(user)
+ end
+
+ it "does not find user by using ldap as fallback to for authentication" do
+ expect(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(nil)
+
+ expect(gl_auth.find_with_user_password('ldap_user', 'password')).to be_nil
end
- it "uses ldap as fallback to for authentication" do
- expect(Gitlab::Auth::LDAP::Authentication).to receive(:login)
+ it "find new user by using ldap as fallback to for authentication" do
+ expect(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(user)
- gl_auth.find_with_user_password('ldap_user', 'password')
+ expect(gl_auth.find_with_user_password('ldap_user', 'password')).to eq(user)
end
end
diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
index 5cb1f4deb5f..0dc3705825d 100644
--- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
@@ -54,7 +54,7 @@ describe ::Gitlab::BareRepositoryImport::Repository do
context 'hashed storage' do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:repository_storage) { 'default' }
- let(:root_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:root_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' }
let(:hashed_path) { "@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" }
let(:repo_path) { File.join(root_path, "#{hashed_path}.git") }
diff --git a/spec/lib/gitlab/checks/project_moved_spec.rb b/spec/lib/gitlab/checks/project_moved_spec.rb
index e263d29656c..8e9386b1ba1 100644
--- a/spec/lib/gitlab/checks/project_moved_spec.rb
+++ b/spec/lib/gitlab/checks/project_moved_spec.rb
@@ -44,44 +44,17 @@ describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do
end
describe '#message' do
- context 'when the push is rejected' do
- it 'returns a redirect message telling the user to try again' do
- project_moved = described_class.new(project, user, 'http', 'foo/bar')
- message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
- "\n\nPlease update your Git remote:" +
- "\n\n git remote set-url origin #{project.http_url_to_repo} and try again.\n"
+ it 'returns a redirect message' do
+ project_moved = described_class.new(project, user, 'http', 'foo/bar')
+ message = <<~MSG
+ Project 'foo/bar' was moved to '#{project.full_path}'.
- expect(project_moved.message(rejected: true)).to eq(message)
- end
- end
+ Please update your Git remote:
- context 'when the push is not rejected' do
- it 'returns a redirect message' do
- project_moved = described_class.new(project, user, 'http', 'foo/bar')
- message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
- "\n\nPlease update your Git remote:" +
- "\n\n git remote set-url origin #{project.http_url_to_repo}\n"
+ git remote set-url origin #{project.http_url_to_repo}
+ MSG
- expect(project_moved.message).to eq(message)
- end
- end
- end
-
- describe '#permanent_redirect?' do
- context 'with a permanent RedirectRoute' do
- it 'returns true' do
- project.route.create_redirect('foo/bar', permanent: true)
- project_moved = described_class.new(project, user, 'http', 'foo/bar')
- expect(project_moved.permanent_redirect?).to be_truthy
- end
- end
-
- context 'without a permanent RedirectRoute' do
- it 'returns false' do
- project.route.create_redirect('foo/bar')
- project_moved = described_class.new(project, user, 'http', 'foo/bar')
- expect(project_moved.permanent_redirect?).to be_falsy
- end
+ expect(project_moved.message).to eq(message)
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
index 1b03227d67b..dc12ba076bc 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
@@ -5,23 +5,23 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
set(:user) { create(:user) }
let(:pipeline) do
- build(:ci_pipeline_with_one_job, project: project,
- ref: 'master')
+ build(:ci_empty_pipeline, project: project, ref: 'master')
end
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
- project: project,
- current_user: user, seeds_block: nil)
+ project: project, current_user: user)
end
let(:step) { described_class.new(pipeline, command) }
- before do
- step.perform!
- end
-
context 'when pipeline is ready to be saved' do
+ before do
+ pipeline.stages.build(name: 'test', project: project)
+
+ step.perform!
+ end
+
it 'saves a pipeline' do
expect(pipeline).to be_persisted
end
@@ -32,6 +32,7 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
it 'creates stages' do
expect(pipeline.reload.stages).to be_one
+ expect(pipeline.stages.first).to be_persisted
end
end
@@ -40,6 +41,10 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
build(:ci_pipeline, project: project, ref: nil)
end
+ before do
+ step.perform!
+ end
+
it 'breaks the chain' do
expect(step.break?).to be true
end
@@ -49,18 +54,4 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
.to include /Failed to persist the pipeline/
end
end
-
- context 'when there is a seed block present' do
- let(:seeds) { spy('pipeline seeds') }
-
- let(:command) do
- double('command', project: project,
- current_user: user,
- seeds_block: seeds)
- end
-
- it 'executes the block' do
- expect(seeds).to have_received(:call).with(pipeline)
- end
- end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
new file mode 100644
index 00000000000..2258ae83f38
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -0,0 +1,153 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Populate do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ let(:pipeline) do
+ build(:ci_pipeline_with_one_job, project: project,
+ ref: 'master')
+ end
+
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ seeds_block: nil)
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ context 'when pipeline doesn not have seeds block' do
+ before do
+ step.perform!
+ end
+
+ it 'does not persist the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'does not break the chain' do
+ expect(step.break?).to be false
+ end
+
+ it 'populates pipeline with stages' do
+ expect(pipeline.stages).to be_one
+ expect(pipeline.stages.first).not_to be_persisted
+ end
+
+ it 'populates pipeline with builds' do
+ expect(pipeline.builds).to be_one
+ expect(pipeline.builds.first).not_to be_persisted
+ expect(pipeline.stages.first.builds).to be_one
+ expect(pipeline.stages.first.builds.first).not_to be_persisted
+ end
+ end
+
+ context 'when pipeline is empty' do
+ let(:config) do
+ { rspec: {
+ script: 'ls',
+ only: ['something']
+ } }
+ end
+
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, config: config)
+ end
+
+ before do
+ step.perform!
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+
+ it 'appends an error about missing stages' do
+ expect(pipeline.errors.to_a)
+ .to include 'No stages / jobs for this pipeline.'
+ end
+ end
+
+ context 'when pipeline has validation errors' do
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, ref: nil)
+ end
+
+ before do
+ step.perform!
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+
+ it 'appends validation error' do
+ expect(pipeline.errors.to_a)
+ .to include 'Failed to build the pipeline!'
+ end
+ end
+
+ context 'when there is a seed blocks present' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ seeds_block: seeds_block)
+ end
+
+ context 'when seeds block builds some resources' do
+ let(:seeds_block) do
+ ->(pipeline) { pipeline.variables.build(key: 'VAR', value: '123') }
+ end
+
+ it 'populates pipeline with resources described in the seeds block' do
+ step.perform!
+
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.variables).not_to be_empty
+ expect(pipeline.variables.first).not_to be_persisted
+ expect(pipeline.variables.first.key).to eq 'VAR'
+ expect(pipeline.variables.first.value).to eq '123'
+ end
+ end
+
+ context 'when seeds block tries to persist some resources' do
+ let(:seeds_block) do
+ ->(pipeline) { pipeline.variables.create!(key: 'VAR', value: '123') }
+ end
+
+ it 'raises exception' do
+ expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved)
+ end
+ end
+ end
+
+ context 'when pipeline gets persisted during the process' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ it 'raises error' do
+ expect { step.perform! }.to raise_error(described_class::PopulateError)
+ end
+ end
+
+ context 'when using only/except build policies' do
+ let(:config) do
+ { rspec: { script: 'rspec', stage: 'test', only: ['master'] },
+ prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] } }
+ end
+
+ let(:pipeline) do
+ build(:ci_pipeline, ref: 'master', config: config)
+ end
+
+ it 'populates pipeline according to used policies' do
+ step.perform!
+
+ expect(pipeline.stages.size).to eq 1
+ expect(pipeline.builds.size).to eq 1
+ expect(pipeline.builds.first.name).to eq 'rspec'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
index 5c12c6e6392..c53294d091c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
@@ -76,28 +76,6 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
end
end
- context 'when pipeline has no stages / jobs' do
- let(:config) do
- { rspec: {
- script: 'ls',
- only: ['something']
- } }
- end
-
- let(:pipeline) do
- build(:ci_pipeline, project: project, config: config)
- end
-
- it 'appends an error about missing stages' do
- expect(pipeline.errors.to_a)
- .to include 'No stages / jobs for this pipeline.'
- end
-
- it 'breaks the chain' do
- expect(step.break?).to be true
- end
- end
-
context 'when pipeline contains configuration validation errors' do
let(:config) { { rspec: {} } }
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
new file mode 100644
index 00000000000..116573379e0
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -0,0 +1,242 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Seed::Build do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ let(:attributes) do
+ { name: 'rspec',
+ ref: 'master',
+ commands: 'rspec' }
+ end
+
+ subject do
+ described_class.new(pipeline, attributes)
+ end
+
+ describe '#attributes' do
+ it 'returns hash attributes of a build' do
+ expect(subject.attributes).to be_a Hash
+ expect(subject.attributes)
+ .to include(:name, :project, :ref, :commands)
+ end
+ end
+
+ describe '#user=' do
+ let(:user) { build(:user) }
+
+ it 'assignes user to a build' do
+ subject.user = user
+
+ expect(subject.attributes).to include(user: user)
+ end
+ end
+
+ describe '#to_resource' do
+ it 'returns a valid build resource' do
+ expect(subject.to_resource).to be_a(::Ci::Build)
+ expect(subject.to_resource).to be_valid
+ end
+
+ it 'memoizes a resource object' do
+ build = subject.to_resource
+
+ expect(build.object_id).to eq subject.to_resource.object_id
+ end
+
+ it 'can not be persisted without explicit assignment' do
+ build = subject.to_resource
+
+ pipeline.save!
+
+ expect(build).not_to be_persisted
+ end
+ end
+
+ describe 'applying only/except policies' do
+ context 'when no branch policy is specified' do
+ let(:attributes) { { name: 'rspec' } }
+
+ it { is_expected.to be_included }
+ end
+
+ context 'when branch policy does not match' do
+ context 'when using only' do
+ let(:attributes) { { name: 'rspec', only: { refs: ['deploy'] } } }
+
+ it { is_expected.not_to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) { { name: 'rspec', except: { refs: ['deploy'] } } }
+
+ it { is_expected.to be_included }
+ end
+ end
+
+ context 'when branch regexp policy does not match' do
+ context 'when using only' do
+ let(:attributes) { { name: 'rspec', only: { refs: ['/^deploy$/'] } } }
+
+ it { is_expected.not_to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) { { name: 'rspec', except: { refs: ['/^deploy$/'] } } }
+
+ it { is_expected.to be_included }
+ end
+ end
+
+ context 'when branch policy matches' do
+ context 'when using only' do
+ let(:attributes) { { name: 'rspec', only: { refs: %w[deploy master] } } }
+
+ it { is_expected.to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) { { name: 'rspec', except: { refs: %w[deploy master] } } }
+
+ it { is_expected.not_to be_included }
+ end
+ end
+
+ context 'when keyword policy matches' do
+ context 'when using only' do
+ let(:attributes) { { name: 'rspec', only: { refs: ['branches'] } } }
+
+ it { is_expected.to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) { { name: 'rspec', except: { refs: ['branches'] } } }
+
+ it { is_expected.not_to be_included }
+ end
+ end
+
+ context 'when keyword policy does not match' do
+ context 'when using only' do
+ let(:attributes) { { name: 'rspec', only: { refs: ['tags'] } } }
+
+ it { is_expected.not_to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) { { name: 'rspec', except: { refs: ['tags'] } } }
+
+ it { is_expected.to be_included }
+ end
+ end
+
+ context 'when keywords and pipeline source policy matches' do
+ possibilities = [%w[pushes push],
+ %w[web web],
+ %w[triggers trigger],
+ %w[schedules schedule],
+ %w[api api],
+ %w[external external]]
+
+ context 'when using only' do
+ possibilities.each do |keyword, source|
+ context "when using keyword `#{keyword}` and source `#{source}`" do
+ let(:pipeline) do
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
+ end
+
+ let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } }
+
+ it { is_expected.to be_included }
+ end
+ end
+ end
+
+ context 'when using except' do
+ possibilities.each do |keyword, source|
+ context "when using keyword `#{keyword}` and source `#{source}`" do
+ let(:pipeline) do
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
+ end
+
+ let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } }
+
+ it { is_expected.not_to be_included }
+ end
+ end
+ end
+ end
+
+ context 'when keywords and pipeline source does not match' do
+ possibilities = [%w[pushes web],
+ %w[web push],
+ %w[triggers schedule],
+ %w[schedules external],
+ %w[api trigger],
+ %w[external api]]
+
+ context 'when using only' do
+ possibilities.each do |keyword, source|
+ context "when using keyword `#{keyword}` and source `#{source}`" do
+ let(:pipeline) do
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
+ end
+
+ let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } }
+
+ it { is_expected.not_to be_included }
+ end
+ end
+ end
+
+ context 'when using except' do
+ possibilities.each do |keyword, source|
+ context "when using keyword `#{keyword}` and source `#{source}`" do
+ let(:pipeline) do
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
+ end
+
+ let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } }
+
+ it { is_expected.to be_included }
+ end
+ end
+ end
+ end
+
+ context 'when repository path matches' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } }
+ end
+
+ it { is_expected.to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } }
+ end
+
+ it { is_expected.not_to be_included }
+ end
+ end
+
+ context 'when repository path does not matches' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: ['branches@fork'] } }
+ end
+
+ it { is_expected.not_to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: ['branches@fork'] } }
+ end
+
+ it { is_expected.to be_included }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
new file mode 100644
index 00000000000..8f0bf40d624
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Seed::Stage do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ let(:attributes) do
+ { name: 'test',
+ index: 0,
+ builds: [{ name: 'rspec' },
+ { name: 'spinach' },
+ { name: 'deploy', only: { refs: ['feature'] } }] }
+ end
+
+ subject do
+ described_class.new(pipeline, attributes)
+ end
+
+ describe '#size' do
+ it 'returns a number of jobs in the stage' do
+ expect(subject.size).to eq 2
+ end
+ end
+
+ describe '#attributes' do
+ it 'returns hash attributes of a stage' do
+ expect(subject.attributes).to be_a Hash
+ expect(subject.attributes).to include(:name, :project)
+ end
+ end
+
+ describe '#included?' do
+ context 'when it contains builds seeds' do
+ let(:attributes) do
+ { name: 'test',
+ index: 0,
+ builds: [{ name: 'deploy', only: { refs: ['master'] } }] }
+ end
+
+ it { is_expected.to be_included }
+ end
+
+ context 'when it does not contain build seeds' do
+ let(:attributes) do
+ { name: 'test',
+ index: 0,
+ builds: [{ name: 'deploy', only: { refs: ['feature'] } }] }
+ end
+
+ it { is_expected.not_to be_included }
+ end
+ end
+
+ describe '#seeds' do
+ it 'returns build seeds' do
+ expect(subject.seeds).to all(be_a Gitlab::Ci::Pipeline::Seed::Build)
+ end
+
+ it 'returns build seeds including valid attributes' do
+ expect(subject.seeds.size).to eq 2
+ expect(subject.seeds.map(&:attributes)).to all(include(ref: 'master'))
+ expect(subject.seeds.map(&:attributes)).to all(include(tag: false))
+ expect(subject.seeds.map(&:attributes)).to all(include(project: pipeline.project))
+ expect(subject.seeds.map(&:attributes))
+ .to all(include(trigger_request: pipeline.trigger_requests.first))
+ end
+
+ context 'when a ref is protected' do
+ before do
+ allow_any_instance_of(Project).to receive(:protected_for?).and_return(true)
+ end
+
+ it 'returns protected builds' do
+ expect(subject.seeds.map(&:attributes)).to all(include(protected: true))
+ end
+ end
+
+ context 'when a ref is not protected' do
+ before do
+ allow_any_instance_of(Project).to receive(:protected_for?).and_return(false)
+ end
+
+ it 'returns unprotected builds' do
+ expect(subject.seeds.map(&:attributes)).to all(include(protected: false))
+ end
+ end
+
+ it 'filters seeds using only/except policies' do
+ expect(subject.seeds.map(&:attributes)).to satisfy do |seeds|
+ seeds.any? { |hash| hash.fetch(:name) == 'rspec' }
+ end
+
+ expect(subject.seeds.map(&:attributes)).not_to satisfy do |seeds|
+ seeds.any? { |hash| hash.fetch(:name) == 'deploy' }
+ end
+ end
+ end
+
+ describe '#user=' do
+ let(:user) { build(:user) }
+
+ it 'assignes relevant pipeline attributes' do
+ subject.user = user
+
+ expect(subject.seeds.map(&:attributes)).to all(include(user: user))
+ end
+ end
+
+ describe '#to_resource' do
+ it 'builds a valid stage object with all builds' do
+ subject.to_resource.save!
+
+ expect(pipeline.reload.stages.count).to eq 1
+ expect(pipeline.reload.builds.count).to eq 2
+ expect(pipeline.builds).to all(satisfy { |job| job.stage_id.present? })
+ expect(pipeline.builds).to all(satisfy { |job| job.pipeline.present? })
+ expect(pipeline.builds).to all(satisfy { |job| job.project.present? })
+ expect(pipeline.stages)
+ .to all(satisfy { |stage| stage.pipeline.present? })
+ expect(pipeline.stages)
+ .to all(satisfy { |stage| stage.project.present? })
+ end
+
+ it 'can not be persisted without explicit pipeline assignment' do
+ stage = subject.to_resource
+
+ pipeline.save!
+
+ expect(stage).not_to be_persisted
+ expect(pipeline.reload.stages.count).to eq 0
+ expect(pipeline.reload.builds.count).to eq 0
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb
deleted file mode 100644
index 3fe8d50c49a..00000000000
--- a/spec/lib/gitlab/ci/stage/seed_spec.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Stage::Seed do
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- let(:builds) do
- [{ name: 'rspec' }, { name: 'spinach' }]
- end
-
- subject do
- described_class.new(pipeline, 'test', builds)
- end
-
- describe '#size' do
- it 'returns a number of jobs in the stage' do
- expect(subject.size).to eq 2
- end
- end
-
- describe '#stage' do
- it 'returns hash attributes of a stage' do
- expect(subject.stage).to be_a Hash
- expect(subject.stage).to include(:name, :project)
- end
- end
-
- describe '#builds' do
- it 'returns hash attributes of all builds' do
- expect(subject.builds.size).to eq 2
- expect(subject.builds).to all(include(ref: 'master'))
- expect(subject.builds).to all(include(tag: false))
- expect(subject.builds).to all(include(project: pipeline.project))
- expect(subject.builds)
- .to all(include(trigger_request: pipeline.trigger_requests.first))
- end
-
- context 'when a ref is protected' do
- before do
- allow_any_instance_of(Project).to receive(:protected_for?).and_return(true)
- end
-
- it 'returns protected builds' do
- expect(subject.builds).to all(include(protected: true))
- end
- end
-
- context 'when a ref is unprotected' do
- before do
- allow_any_instance_of(Project).to receive(:protected_for?).and_return(false)
- end
-
- it 'returns unprotected builds' do
- expect(subject.builds).to all(include(protected: false))
- end
- end
- end
-
- describe '#user=' do
- let(:user) { build(:user) }
-
- it 'assignes relevant pipeline attributes' do
- subject.user = user
-
- expect(subject.builds).to all(include(user: user))
- end
- end
-
- describe '#create!' do
- it 'creates all stages and builds' do
- subject.create!
-
- expect(pipeline.reload.stages.count).to eq 1
- expect(pipeline.reload.builds.count).to eq 2
- expect(pipeline.builds).to all(satisfy { |job| job.stage_id.present? })
- expect(pipeline.builds).to all(satisfy { |job| job.pipeline.present? })
- expect(pipeline.builds).to all(satisfy { |job| job.project.present? })
- expect(pipeline.stages)
- .to all(satisfy { |stage| stage.pipeline.present? })
- expect(pipeline.stages)
- .to all(satisfy { |stage| stage.project.present? })
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/trace/http_io_spec.rb b/spec/lib/gitlab/ci/trace/http_io_spec.rb
new file mode 100644
index 00000000000..5474e2f518c
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace/http_io_spec.rb
@@ -0,0 +1,315 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Trace::HttpIO do
+ include HttpIOHelpers
+
+ let(:http_io) { described_class.new(url, size) }
+ let(:url) { remote_trace_url }
+ let(:size) { remote_trace_size }
+
+ describe '#close' do
+ subject { http_io.close }
+
+ it { is_expected.to be_nil }
+ end
+
+ describe '#binmode' do
+ subject { http_io.binmode }
+
+ it { is_expected.to be_nil }
+ end
+
+ describe '#binmode?' do
+ subject { http_io.binmode? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#path' do
+ subject { http_io.path }
+
+ it { is_expected.to be_nil }
+ end
+
+ describe '#url' do
+ subject { http_io.url }
+
+ it { is_expected.to eq(url) }
+ end
+
+ describe '#seek' do
+ subject { http_io.seek(pos, where) }
+
+ context 'when moves pos to end of the file' do
+ let(:pos) { 0 }
+ let(:where) { IO::SEEK_END }
+
+ it { is_expected.to eq(size) }
+ end
+
+ context 'when moves pos to middle of the file' do
+ let(:pos) { size / 2 }
+ let(:where) { IO::SEEK_SET }
+
+ it { is_expected.to eq(size / 2) }
+ end
+
+ context 'when moves pos around' do
+ it 'matches the result' do
+ expect(http_io.seek(0)).to eq(0)
+ expect(http_io.seek(100, IO::SEEK_CUR)).to eq(100)
+ expect { http_io.seek(size + 1, IO::SEEK_CUR) }.to raise_error('new position is outside of file')
+ end
+ end
+ end
+
+ describe '#eof?' do
+ subject { http_io.eof? }
+
+ context 'when current pos is at end of the file' do
+ before do
+ http_io.seek(size, IO::SEEK_SET)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when current pos is not at end of the file' do
+ before do
+ http_io.seek(0, IO::SEEK_SET)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#each_line' do
+ subject { http_io.each_line }
+
+ let(:string_io) { StringIO.new(remote_trace_body) }
+
+ before do
+ stub_remote_trace_206
+ end
+
+ it 'yields lines' do
+ expect { |b| http_io.each_line(&b) }.to yield_successive_args(*string_io.each_line.to_a)
+ end
+
+ context 'when buckets on GCS' do
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ stub_remote_trace_200
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'calls get_chunk only once' do
+ expect_any_instance_of(Net::HTTP).to receive(:request).once.and_call_original
+
+ http_io.each_line { |line| }
+ end
+ end
+ end
+ end
+
+ describe '#read' do
+ subject { http_io.read(length) }
+
+ context 'when there are no network issue' do
+ before do
+ stub_remote_trace_206
+ end
+
+ context 'when read whole size' do
+ let(:length) { nil }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body)
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body)
+ end
+ end
+ end
+
+ context 'when read only first 100 bytes' do
+ let(:length) { 100 }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body[0, length])
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body[0, length])
+ end
+ end
+ end
+
+ context 'when tries to read oversize' do
+ let(:length) { size + 1000 }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body)
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body)
+ end
+ end
+ end
+
+ context 'when tries to read 0 bytes' do
+ let(:length) { 0 }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when there is anetwork issue' do
+ let(:length) { nil }
+
+ before do
+ stub_remote_trace_500
+ end
+
+ it 'reads a trace' do
+ expect { subject }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError)
+ end
+ end
+ end
+
+ describe '#readline' do
+ subject { http_io.readline }
+
+ let(:string_io) { StringIO.new(remote_trace_body) }
+
+ before do
+ stub_remote_trace_206
+ end
+
+ shared_examples 'all line matching' do
+ it 'reads a line' do
+ (0...remote_trace_body.lines.count).each do
+ expect(http_io.readline).to eq(string_io.readline)
+ end
+ end
+ end
+
+ context 'when there is anetwork issue' do
+ let(:length) { nil }
+
+ before do
+ stub_remote_trace_500
+ end
+
+ it 'reads a trace' do
+ expect { subject }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError)
+ end
+ end
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it_behaves_like 'all line matching'
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it_behaves_like 'all line matching'
+ end
+
+ context 'when pos is at middle of the file' do
+ before do
+ set_smaller_buffer_size_than(size)
+
+ http_io.seek(size / 2)
+ string_io.seek(size / 2)
+ end
+
+ it 'reads from pos' do
+ expect(http_io.readline).to eq(string_io.readline)
+ end
+ end
+ end
+
+ describe '#write' do
+ subject { http_io.write(nil) }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#truncate' do
+ subject { http_io.truncate(nil) }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#flush' do
+ subject { http_io.flush }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#present?' do
+ subject { http_io.present? }
+
+ it { is_expected.to be_truthy }
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 448c6fb57dd..3a9371ed2e8 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -510,6 +510,28 @@ describe Gitlab::Ci::Trace do
it_behaves_like 'source trace in database stays intact', error: ActiveRecord::RecordInvalid
end
+
+ context 'when there is a validation error on Ci::Build' do
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::Build).to receive_message_chain(:errors, :full_messages)
+ .and_return(%w[Error Error])
+ end
+
+ context "when erase old trace with 'save'" do
+ before do
+ build.send(:write_attribute, :trace, nil)
+ build.save
+ end
+
+ it 'old trace is not deleted' do
+ build.reload
+ expect(build.trace.raw).to eq(trace_content)
+ end
+ end
+
+ it_behaves_like 'archive trace in database'
+ end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index f83f932e61e..fbc2af29b98 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -18,6 +18,34 @@ module Gitlab
describe '#build_attributes' do
subject { described_class.new(config).build_attributes(:rspec) }
+ describe 'attributes list' do
+ let(:config) do
+ YAML.dump(
+ before_script: ['pwd'],
+ rspec: { script: 'rspec' }
+ )
+ end
+
+ it 'returns valid build attributes' do
+ expect(subject).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+ end
+
describe 'coverage entry' do
describe 'code coverage regexp' do
let(:config) do
@@ -105,512 +133,118 @@ module Gitlab
end
end
- describe '#stage_seeds' do
- context 'when no refs policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod' },
- rspec: { stage: 'test', script: 'rspec' },
- spinach: { stage: 'test', script: 'spinach' })
- end
-
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- it 'correctly fabricates a stage seeds object' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 2
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.second.stage[:name]).to eq 'deploy'
- expect(seeds.first.builds.dig(0, :name)).to eq 'rspec'
- expect(seeds.first.builds.dig(1, :name)).to eq 'spinach'
- expect(seeds.second.builds.dig(0, :name)).to eq 'production'
- end
- end
-
- context 'when refs policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
- spinach: { stage: 'test', script: 'spinach', only: ['tags'] })
- end
-
- let(:pipeline) do
- create(:ci_empty_pipeline, ref: 'feature', tag: true)
- end
-
- it 'returns stage seeds only assigned to master to master' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
- end
-
- context 'when source policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
- spinach: { stage: 'test', script: 'spinach', only: ['schedules'] })
- end
-
- let(:pipeline) do
- create(:ci_empty_pipeline, source: :schedule)
- end
-
- it 'returns stage seeds only assigned to schedules' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
+ describe '#stages_attributes' do
+ let(:config) do
+ YAML.dump(
+ rspec: { script: 'rspec', stage: 'test', only: ['branches'] },
+ prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] }
+ )
end
- context 'when kubernetes policy is specified' do
- let(:config) do
- YAML.dump(
- spinach: { stage: 'test', script: 'spinach' },
- production: {
- stage: 'deploy',
- script: 'cap',
- only: { kubernetes: 'active' }
- }
- )
- end
-
- context 'when kubernetes is active' do
- shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
- it 'returns seeds for kubernetes dependent job' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 2
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- expect(seeds.second.builds.dig(0, :name)).to eq 'production'
- end
- end
-
- context 'when user configured kubernetes from Integration > Kubernetes' do
- let(:project) { create(:kubernetes_project) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
-
- it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
- end
-
- context 'when user configured kubernetes from CI/CD > Clusters' do
- let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
-
- it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
- end
- end
-
- context 'when kubernetes is not active' do
- it 'does not return seeds for kubernetes dependent job' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
- end
+ let(:attributes) do
+ [{ name: "build",
+ index: 0,
+ builds: [] },
+ { name: "test",
+ index: 1,
+ builds:
+ [{ stage_idx: 1,
+ stage: "test",
+ commands: "rspec",
+ tag_list: [],
+ name: "rspec",
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ coverage_regex: nil,
+ yaml_variables: [],
+ options: { script: ["rspec"] },
+ only: { refs: ["branches"] },
+ except: {} }] },
+ { name: "deploy",
+ index: 2,
+ builds:
+ [{ stage_idx: 2,
+ stage: "deploy",
+ commands: "cap prod",
+ tag_list: [],
+ name: "prod",
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ coverage_regex: nil,
+ yaml_variables: [],
+ options: { script: ["cap prod"] },
+ only: { refs: ["tags"] },
+ except: {} }] }]
+ end
+
+ it 'returns stages seed attributes' do
+ expect(subject.stages_attributes).to eq attributes
end
end
- describe "#pipeline_stage_builds" do
- let(:type) { 'test' }
+ describe 'only / except policies validations' do
+ context 'when `only` has an invalid value' do
+ let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
+ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
- it "returns builds if no branch specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec" }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
-
- describe 'only' do
- it "does not return builds if only has another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["deploy"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
- end
-
- it "does not return builds if only has regexp with another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["/^deploy$/"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
- end
-
- it "returns builds if only has specified this branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["master"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
- end
-
- it "returns builds if only has a list of branches including specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: %w(master deploy) }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
- end
-
- it "returns builds if only has a branches keyword specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
- end
-
- it "does not return builds if only has a tags keyword" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["tags"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
- end
+ context 'when it is integer' do
+ let(:only) { 1 }
- it "returns builds if only has special keywords specified and source matches" do
- possibilities = [{ keyword: 'pushes', source: 'push' },
- { keyword: 'web', source: 'web' },
- { keyword: 'triggers', source: 'trigger' },
- { keyword: 'schedules', source: 'schedule' },
- { keyword: 'api', source: 'api' },
- { keyword: 'external', source: 'external' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1)
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only has to be either an array of conditions or a hash')
end
end
- it "does not return builds if only has special keywords specified and source doesn't match" do
- possibilities = [{ keyword: 'pushes', source: 'web' },
- { keyword: 'web', source: 'push' },
- { keyword: 'triggers', source: 'schedule' },
- { keyword: 'schedules', source: 'external' },
- { keyword: 'api', source: 'trigger' },
- { keyword: 'external', source: 'api' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ context 'when it is an array of integers' do
+ let(:only) { [1, 1] }
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0)
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only config should be an array of strings or regexps')
end
end
- it "returns builds if only has current repository path" do
- seed_pipeline = pipeline(ref: 'deploy')
-
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: {
- script: "rspec",
- type: type,
- only: ["branches@#{seed_pipeline.project_full_path}"]
- }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ context 'when it is invalid regex' do
+ let(:only) { ["/*invalid/"] }
- expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(1)
- end
-
- it "does not return builds if only has different repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches@fork"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
- end
-
- it "returns build only for specified type" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", only: %w(master deploy) },
- staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
- production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "deploy")).size).to eq(2)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "deploy")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "master")).size).to eq(1)
- end
-
- context 'for invalid value' do
- let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
- let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
-
- context 'when it is integer' do
- let(:only) { 1 }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only has to be either an array of conditions or a hash')
- end
- end
-
- context 'when it is an array of integers' do
- let(:only) { [1, 1] }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only config should be an array of strings or regexps')
- end
- end
-
- context 'when it is invalid regex' do
- let(:only) { ["/*invalid/"] }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only config should be an array of strings or regexps')
- end
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only config should be an array of strings or regexps')
end
end
end
- describe 'except' do
- it "returns builds if except has another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["deploy"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
- end
-
- it "returns builds if except has regexp with another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["/^deploy$/"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ context 'when `except` has an invalid value' do
+ let(:config) { { rspec: { script: "rspec", except: except } } }
+ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
- end
-
- it "does not return builds if except has specified this branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["master"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
- end
-
- it "does not return builds if except has a list of branches including specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: %w(master deploy) }
- })
+ context 'when it is integer' do
+ let(:except) { 1 }
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
- end
-
- it "does not return builds if except has a branches keyword specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
- end
-
- it "returns builds if except has a tags keyword" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["tags"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
- end
-
- it "does not return builds if except has special keywords specified and source matches" do
- possibilities = [{ keyword: 'pushes', source: 'push' },
- { keyword: 'web', source: 'web' },
- { keyword: 'triggers', source: 'trigger' },
- { keyword: 'schedules', source: 'schedule' },
- { keyword: 'api', source: 'api' },
- { keyword: 'external', source: 'external' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0)
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except has to be either an array of conditions or a hash')
end
end
- it "returns builds if except has special keywords specified and source doesn't match" do
- possibilities = [{ keyword: 'pushes', source: 'web' },
- { keyword: 'web', source: 'push' },
- { keyword: 'triggers', source: 'schedule' },
- { keyword: 'schedules', source: 'external' },
- { keyword: 'api', source: 'trigger' },
- { keyword: 'external', source: 'api' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
- })
+ context 'when it is an array of integers' do
+ let(:except) { [1, 1] }
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1)
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except config should be an array of strings or regexps')
end
end
- it "does not return builds if except has current repository path" do
- seed_pipeline = pipeline(ref: 'deploy')
-
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: {
- script: "rspec",
- type: type,
- except: ["branches@#{seed_pipeline.project_full_path}"]
- }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(0)
- end
-
- it "returns builds if except has different repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches@fork"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
- end
-
- it "returns build except specified type" do
- master_pipeline = pipeline(ref: 'master')
- test_pipeline = pipeline(ref: 'test')
- deploy_pipeline = pipeline(ref: 'deploy')
-
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@#{test_pipeline.project_full_path}"] },
- staging: { script: "deploy", type: "deploy", except: ["master"] },
- production: { script: "deploy", type: "deploy", except: ["master@#{master_pipeline.project_full_path}"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds("deploy", deploy_pipeline).size).to eq(2)
- expect(config_processor.pipeline_stage_builds("test", test_pipeline).size).to eq(0)
- expect(config_processor.pipeline_stage_builds("deploy", master_pipeline).size).to eq(0)
- end
-
- context 'for invalid value' do
- let(:config) { { rspec: { script: "rspec", except: except } } }
- let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+ context 'when it is invalid regex' do
+ let(:except) { ["/*invalid/"] }
- context 'when it is integer' do
- let(:except) { 1 }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:except has to be either an array of conditions or a hash')
- end
- end
-
- context 'when it is an array of integers' do
- let(:except) { [1, 1] }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:except config should be an array of strings or regexps')
- end
- end
-
- context 'when it is invalid regex' do
- let(:except) { ["/*invalid/"] }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:except config should be an array of strings or regexps')
- end
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except config should be an array of strings or regexps')
end
end
end
@@ -620,7 +254,7 @@ module Gitlab
let(:config_data) { YAML.dump(config) }
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data) }
- subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first }
+ subject { config_processor.stage_builds_attributes('test').first }
describe "before_script" do
context "in global context" do
@@ -703,8 +337,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -738,8 +372,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -771,8 +405,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -800,8 +434,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -946,8 +580,8 @@ module Gitlab
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ builds = config_processor.stage_builds_attributes("test")
- builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master"))
expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq(when_state)
end
@@ -978,8 +612,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
@@ -997,8 +631,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
@@ -1017,8 +651,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
paths: ["test/"],
untracked: false,
key: 'local',
@@ -1046,8 +680,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -1083,8 +717,8 @@ module Gitlab
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ builds = config_processor.stage_builds_attributes("test")
- builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master"))
expect(builds.size).to eq(1)
expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
end
@@ -1099,7 +733,7 @@ module Gitlab
end
let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
- let(:builds) { processor.pipeline_stage_builds('deploy', pipeline(ref: 'master')) }
+ let(:builds) { processor.stage_builds_attributes('deploy') }
context 'when a production environment is specified' do
let(:environment) { 'production' }
@@ -1256,7 +890,7 @@ module Gitlab
describe "Hidden jobs" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
- subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) }
+ subject { config_processor.stage_builds_attributes("test") }
shared_examples 'hidden_job_handling' do
it "doesn't create jobs that start with dot" do
@@ -1304,7 +938,7 @@ module Gitlab
describe "YAML Alias/Anchor" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
- subject { config_processor.pipeline_stage_builds("build", pipeline(ref: "master")) }
+ subject { config_processor.stage_builds_attributes("build") }
shared_examples 'job_templates_handling' do
it "is correctly supported for jobs" do
@@ -1344,13 +978,13 @@ module Gitlab
context 'when template is a job' do
let(:config) do
- <<EOT
-job1: &JOBTMPL
- stage: build
- script: execute-script-for-job
+ <<~EOT
+ job1: &JOBTMPL
+ stage: build
+ script: execute-script-for-job
-job2: *JOBTMPL
-EOT
+ job2: *JOBTMPL
+ EOT
end
it_behaves_like 'job_templates_handling'
@@ -1358,15 +992,15 @@ EOT
context 'when template is a hidden job' do
let(:config) do
- <<EOT
-.template: &JOBTMPL
- stage: build
- script: execute-script-for-job
+ <<~EOT
+ .template: &JOBTMPL
+ stage: build
+ script: execute-script-for-job
-job1: *JOBTMPL
+ job1: *JOBTMPL
-job2: *JOBTMPL
-EOT
+ job2: *JOBTMPL
+ EOT
end
it_behaves_like 'job_templates_handling'
@@ -1374,18 +1008,18 @@ EOT
context 'when job adds its own keys to a template definition' do
let(:config) do
- <<EOT
-.template: &JOBTMPL
- stage: build
-
-job1:
- <<: *JOBTMPL
- script: execute-script-for-job
-
-job2:
- <<: *JOBTMPL
- script: execute-script-for-job
-EOT
+ <<~EOT
+ .template: &JOBTMPL
+ stage: build
+
+ job1:
+ <<: *JOBTMPL
+ script: execute-script-for-job
+
+ job2:
+ <<: *JOBTMPL
+ script: execute-script-for-job
+ EOT
end
it_behaves_like 'job_templates_handling'
@@ -1724,10 +1358,6 @@ EOT
it { is_expected.to be_nil }
end
end
-
- def pipeline(**attributes)
- build_stubbed(:ci_empty_pipeline, **attributes)
- end
end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 1de3a14b809..280f799f2ab 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -67,17 +67,35 @@ describe Gitlab::Database::MigrationHelpers do
model.add_concurrent_index(:users, :foo, unique: true)
end
+
+ it 'does nothing if the index exists already' do
+ expect(model).to receive(:index_exists?)
+ .with(:users, :foo, { algorithm: :concurrently, unique: true }).and_return(true)
+ expect(model).not_to receive(:add_index)
+
+ model.add_concurrent_index(:users, :foo, unique: true)
+ end
end
context 'using MySQL' do
- it 'creates a regular index' do
- expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+ it 'creates a regular index' do
expect(model).to receive(:add_index)
.with(:users, :foo, {})
model.add_concurrent_index(:users, :foo)
end
+
+ it 'does nothing if the index exists already' do
+ expect(model).to receive(:index_exists?)
+ .with(:users, :foo, { unique: true }).and_return(true)
+ expect(model).not_to receive(:add_index)
+
+ model.add_concurrent_index(:users, :foo, unique: true)
+ end
end
end
@@ -95,6 +113,7 @@ describe Gitlab::Database::MigrationHelpers do
context 'outside a transaction' do
before do
allow(model).to receive(:transaction_open?).and_return(false)
+ allow(model).to receive(:index_exists?).and_return(true)
end
context 'using PostgreSQL' do
@@ -103,18 +122,41 @@ describe Gitlab::Database::MigrationHelpers do
allow(model).to receive(:disable_statement_timeout)
end
- it 'removes the index concurrently by column name' do
- expect(model).to receive(:remove_index)
- .with(:users, { algorithm: :concurrently, column: :foo })
+ describe 'by column name' do
+ it 'removes the index concurrently' do
+ expect(model).to receive(:remove_index)
+ .with(:users, { algorithm: :concurrently, column: :foo })
- model.remove_concurrent_index(:users, :foo)
+ model.remove_concurrent_index(:users, :foo)
+ end
+
+ it 'does nothing if the index does not exist' do
+ expect(model).to receive(:index_exists?)
+ .with(:users, :foo, { algorithm: :concurrently, unique: true }).and_return(false)
+ expect(model).not_to receive(:remove_index)
+
+ model.remove_concurrent_index(:users, :foo, unique: true)
+ end
end
- it 'removes the index concurrently by index name' do
- expect(model).to receive(:remove_index)
- .with(:users, { algorithm: :concurrently, name: "index_x_by_y" })
+ describe 'by index name' do
+ before do
+ allow(model).to receive(:index_exists_by_name?).with(:users, "index_x_by_y").and_return(true)
+ end
+
+ it 'removes the index concurrently by index name' do
+ expect(model).to receive(:remove_index)
+ .with(:users, { algorithm: :concurrently, name: "index_x_by_y" })
+
+ model.remove_concurrent_index_by_name(:users, "index_x_by_y")
+ end
+
+ it 'does nothing if the index does not exist' do
+ expect(model).to receive(:index_exists_by_name?).with(:users, "index_x_by_y").and_return(false)
+ expect(model).not_to receive(:remove_index)
- model.remove_concurrent_index_by_name(:users, "index_x_by_y")
+ model.remove_concurrent_index_by_name(:users, "index_x_by_y")
+ end
end
end
@@ -141,6 +183,10 @@ describe Gitlab::Database::MigrationHelpers do
end
describe '#add_concurrent_foreign_key' do
+ before do
+ allow(model).to receive(:foreign_key_exists?).and_return(false)
+ end
+
context 'inside a transaction' do
it 'raises an error' do
expect(model).to receive(:transaction_open?).and_return(true)
@@ -157,14 +203,23 @@ describe Gitlab::Database::MigrationHelpers do
end
context 'using MySQL' do
- it 'creates a regular foreign key' do
+ before do
allow(Gitlab::Database).to receive(:mysql?).and_return(true)
+ end
+ it 'creates a regular foreign key' do
expect(model).to receive(:add_foreign_key)
.with(:projects, :users, column: :user_id, on_delete: :cascade)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
end
+
+ it 'does not create a foreign key if it exists already' do
+ expect(model).to receive(:foreign_key_exists?).with(:projects, :users, column: :user_id).and_return(true)
+ expect(model).not_to receive(:add_foreign_key)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end
end
context 'using PostgreSQL' do
@@ -189,6 +244,14 @@ describe Gitlab::Database::MigrationHelpers do
column: :user_id,
on_delete: :nullify)
end
+
+ it 'does not create a foreign key if it exists already' do
+ expect(model).to receive(:foreign_key_exists?).with(:projects, :users, column: :user_id).and_return(true)
+ expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/)
+ expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end
end
end
end
@@ -203,6 +266,29 @@ describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#foreign_key_exists?' do
+ before do
+ key = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(:projects, :users, { column: :non_standard_id })
+ allow(model).to receive(:foreign_keys).with(:projects).and_return([key])
+ end
+
+ it 'finds existing foreign keys by column' do
+ expect(model.foreign_key_exists?(:projects, :users, column: :non_standard_id)).to be_truthy
+ end
+
+ it 'finds existing foreign keys by target table only' do
+ expect(model.foreign_key_exists?(:projects, :users)).to be_truthy
+ end
+
+ it 'compares by column name if given' do
+ expect(model.foreign_key_exists?(:projects, :users, column: :user_id)).to be_falsey
+ end
+
+ it 'compares by target if no column given' do
+ expect(model.foreign_key_exists?(:projects, :other_table)).to be_falsey
+ end
+ end
+
describe '#disable_statement_timeout' do
context 'using PostgreSQL' do
it 'disables statement timeouts' do
@@ -1125,4 +1211,33 @@ describe Gitlab::Database::MigrationHelpers do
expect(model.perform_background_migration_inline?).to eq(false)
end
end
+
+ describe '#index_exists_by_name?' do
+ it 'returns true if an index exists' do
+ expect(model.index_exists_by_name?(:projects, 'index_projects_on_path'))
+ .to be_truthy
+ end
+
+ it 'returns false if the index does not exist' do
+ expect(model.index_exists_by_name?(:projects, 'this_does_not_exist'))
+ .to be_falsy
+ end
+
+ context 'when an index with a function exists', :postgresql do
+ before do
+ ActiveRecord::Base.connection.execute(
+ 'CREATE INDEX test_index ON projects (LOWER(path));'
+ )
+ end
+
+ after do
+ 'DROP INDEX IF EXISTS test_index;'
+ end
+
+ it 'returns true if an index exists' do
+ expect(model.index_exists_by_name?(:projects, 'test_index'))
+ .to be_truthy
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 9204ea37963..0c2e18c268a 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -455,5 +455,17 @@ describe Gitlab::Diff::File do
expect(diff_file.size).to be_zero
end
end
+
+ describe '#different_type?' do
+ it 'returns false' do
+ expect(diff_file).not_to be_different_type
+ end
+ end
+
+ describe '#content_changed?' do
+ it 'returns false' do
+ expect(diff_file).not_to be_content_changed
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 83d431a7458..e68c9850f6b 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -161,6 +161,11 @@ describe Gitlab::EncodingHelper do
'removes invalid bytes from ASCII-8bit encoded multibyte string.',
"Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'),
"Lorem ipsum\n dolor sit amet, xyàyùabcdùefg"
+ ],
+ [
+ 'handles UTF-16BE encoded strings',
+ "\xFE\xFF\x00\x41".force_encoding('ASCII-8BIT'), # An "A" prepended with UTF-16 BOM
+ "\xEF\xBB\xBFA" # An "A" prepended with UTF-8 BOM
]
].each do |description, test_string, xpect|
it description do
diff --git a/spec/lib/gitlab/git/conflict/file_spec.rb b/spec/lib/gitlab/git/conflict/file_spec.rb
new file mode 100644
index 00000000000..afed6c32af6
--- /dev/null
+++ b/spec/lib/gitlab/git/conflict/file_spec.rb
@@ -0,0 +1,50 @@
+# coding: utf-8
+require 'spec_helper'
+
+describe Gitlab::Git::Conflict::File do
+ let(:conflict) { { theirs: { path: 'foo', mode: 33188 }, ours: { path: 'foo', mode: 33188 } } }
+ let(:invalid_content) { described_class.new(nil, nil, conflict, "a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }
+ let(:valid_content) { described_class.new(nil, nil, conflict, "Espa\xC3\xB1a".force_encoding(Encoding::ASCII_8BIT)) }
+
+ describe '#lines' do
+ context 'when the content contains non-UTF-8 characters' do
+ it 'raises UnsupportedEncoding' do
+ expect { invalid_content.lines }
+ .to raise_error(described_class::UnsupportedEncoding)
+ end
+ end
+
+ context 'when the content can be converted to UTF-8' do
+ it 'sets lines to the lines' do
+ expect(valid_content.lines).to eq([{
+ full_line: 'España',
+ type: nil,
+ line_obj_index: 0,
+ line_old: 1,
+ line_new: 1
+ }])
+ end
+
+ it 'sets the type to text' do
+ expect(valid_content.type).to eq('text')
+ end
+ end
+ end
+
+ describe '#content' do
+ context 'when the content contains non-UTF-8 characters' do
+ it 'raises UnsupportedEncoding' do
+ expect { invalid_content.content }
+ .to raise_error(described_class::UnsupportedEncoding)
+ end
+ end
+
+ context 'when the content can be converted to UTF-8' do
+ it 'returns a valid UTF-8 string' do
+ expect(valid_content.content).to eq('España')
+ expect(valid_content.content).to be_valid_encoding
+ expect(valid_content.content.encoding).to eq(Encoding::UTF_8)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/conflict/parser_spec.rb b/spec/lib/gitlab/git/conflict/parser_spec.rb
index 7b035a381f1..29a1702a1c6 100644
--- a/spec/lib/gitlab/git/conflict/parser_spec.rb
+++ b/spec/lib/gitlab/git/conflict/parser_spec.rb
@@ -212,13 +212,6 @@ CONFLICT
.not_to raise_error
end
end
-
- context 'when the file contains non-UTF-8 characters' do
- it 'raises UnsupportedEncoding' do
- expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }
- .to raise_error(Gitlab::Git::Conflict::Parser::UnsupportedEncoding)
- end
- end
end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 54ada3e423f..0e315b3f49e 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -604,17 +604,20 @@ describe Gitlab::Git::Repository, seed_helper: true do
shared_examples 'returning the right branches' do
let(:head_id) { repository.rugged.head.target.oid }
let(:new_branch) { head_id }
+ let(:utf8_branch) { 'branch-é' }
before do
repository.create_branch(new_branch, 'master')
+ repository.create_branch(utf8_branch, 'master')
end
after do
repository.delete_branch(new_branch)
+ repository.delete_branch(utf8_branch)
end
it 'displays that branch' do
- expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch)
+ expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch)
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 6f07e423c1b..f8f09d29c73 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -240,14 +240,21 @@ describe Gitlab::GitAccess do
end
shared_examples 'check_project_moved' do
- it 'enqueues a redirected message' do
+ it 'enqueues a redirected message for pushing' do
push_access_check
expect(Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id)).not_to be_nil
end
+
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
end
- describe '#check_project_moved!', :clean_gitlab_redis_shared_state do
+ describe '#add_project_moved_message!', :clean_gitlab_redis_shared_state do
before do
project.add_master(user)
end
@@ -261,62 +268,18 @@ describe Gitlab::GitAccess do
end
end
- context 'when a permanent redirect and ssh protocol' do
+ context 'with a redirect and ssh protocol' do
let(:redirected_path) { 'some/other-path' }
- before do
- allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
- end
-
- it 'allows push and pull access' do
- aggregate_failures do
- expect { push_access_check }.not_to raise_error
- end
- end
-
it_behaves_like 'check_project_moved'
end
- context 'with a permanent redirect and http protocol' do
+ context 'with a redirect and http protocol' do
let(:redirected_path) { 'some/other-path' }
let(:protocol) { 'http' }
- before do
- allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
- end
-
- it 'allows_push and pull access' do
- aggregate_failures do
- expect { push_access_check }.not_to raise_error
- end
- end
-
it_behaves_like 'check_project_moved'
end
-
- context 'with a temporal redirect and ssh protocol' do
- let(:redirected_path) { 'some/other-path' }
-
- it 'blocks push and pull access' do
- aggregate_failures do
- expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /Project '#{redirected_path}' was moved to '#{project.full_path}'/)
- expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/)
-
- expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /Project '#{redirected_path}' was moved to '#{project.full_path}'/)
- expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/)
- end
- end
- end
-
- context 'with a temporal redirect and http protocol' do
- let(:redirected_path) { 'some/other-path' }
- let(:protocol) { 'http' }
-
- it 'does not allow to push and pull access' do
- expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
- expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
- end
- end
end
describe '#check_authentication_abilities!' do
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
index 872377c93d8..f03c7e3f04b 100644
--- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -58,4 +58,14 @@ describe Gitlab::GitalyClient::RemoteService do
client.update_remote_mirror(ref_name, only_branches_matching)
end
end
+
+ describe '.exists?' do
+ context "when the remote doesn't exist" do
+ let(:url) { 'https://gitlab.com/gitlab-org/ik-besta-niet-of-ik-word-geplaagd.git' }
+
+ it 'returns false' do
+ expect(described_class.exists?(url)).to be(false)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 5bedfc79dd3..1f0f1fdd7da 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -38,8 +38,12 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
expect(project)
.to receive(:wiki_repository_exists?)
.and_return(false)
+ expect(Gitlab::GitalyClient::RemoteService)
+ .to receive(:exists?)
+ .with("foo.wiki.git")
+ .and_return(true)
- expect(importer.import_wiki?).to eq(true)
+ expect(importer.import_wiki?).to be(true)
end
it 'returns false if the GitHub wiki is disabled' do
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index 4c1ca4349ea..9dcf272d25e 100644
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -26,7 +26,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
let(:storages_paths) do
{
- default: { path: tmp_dir }
+ default: Gitlab::GitalyClient::StorageSettings.new('path' => tmp_dir)
}.with_indifferent_access
end
@@ -56,7 +56,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
context 'storage points to not existing folder' do
let(:storages_paths) do
{
- default: { path: 'tmp/this/path/doesnt/exist' }
+ default: Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/this/path/doesnt/exist')
}.with_indifferent_access
end
@@ -102,7 +102,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
context 'storage points to not existing folder' do
let(:storages_paths) do
{
- default: { path: 'tmp/this/path/doesnt/exist' }
+ default: Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/this/path/doesnt/exist')
}.with_indifferent_access
end
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
new file mode 100644
index 00000000000..b0bc081a3c8
--- /dev/null
+++ b/spec/lib/gitlab/http_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::HTTP do
+ describe 'allow_local_requests_from_hooks_and_services is' do
+ before do
+ WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success')
+ end
+
+ context 'disabled' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_hooks_and_services?).and_return(false)
+ end
+
+ it 'deny requests to localhost' do
+ expect { described_class.get('http://localhost:3003') }.to raise_error(URI::InvalidURIError)
+ end
+
+ it 'deny requests to private network' do
+ expect { described_class.get('http://192.168.1.2:3003') }.to raise_error(URI::InvalidURIError)
+ end
+
+ context 'if allow_local_requests set to true' do
+ it 'override the global value and allow requests to localhost or private network' do
+ expect { described_class.get('http://localhost:3003', allow_local_requests: true) }.not_to raise_error
+ end
+ end
+ end
+
+ context 'enabled' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_hooks_and_services?).and_return(true)
+ end
+
+ it 'allow requests to localhost' do
+ expect { described_class.get('http://localhost:3003') }.not_to raise_error
+ end
+
+ it 'allow requests to private network' do
+ expect { described_class.get('http://192.168.1.2:3003') }.not_to raise_error
+ end
+
+ context 'if allow_local_requests set to false' do
+ it 'override the global value and ban requests to localhost or private network' do
+ expect { described_class.get('http://localhost:3003', allow_local_requests: false) }.to raise_error(URI::InvalidURIError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index bece82e531a..a204a8f1ffe 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -279,6 +279,7 @@ project:
- lfs_file_locks
- project_badges
- source_of_merge_requests
+- internal_ids
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 0b938892da5..0716852f57f 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -265,7 +265,9 @@ CommitStatus:
- target_url
- description
- artifacts_file
+- artifacts_file_store
- artifacts_metadata
+- artifacts_metadata_store
- erased_by_id
- erased_at
- artifacts_expire_at
@@ -458,6 +460,7 @@ Project:
- merge_requests_ff_only_enabled
- merge_requests_rebase_enabled
- jobs_cache_index
+- pages_https_only
Author:
- name
ProjectFeature:
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
new file mode 100644
index 00000000000..d808b4d49e0
--- /dev/null
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::OmniauthInitializer do
+ let(:devise_config) { class_double(Devise) }
+
+ subject { described_class.new(devise_config) }
+
+ describe '#execute' do
+ it 'configures providers from array' do
+ generic_config = { 'name' => 'generic' }
+
+ expect(devise_config).to receive(:omniauth).with(:generic)
+
+ subject.execute([generic_config])
+ end
+
+ it 'allows "args" array for app_id and app_secret' do
+ legacy_config = { 'name' => 'legacy', 'args' => %w(123 abc) }
+
+ expect(devise_config).to receive(:omniauth).with(:legacy, '123', 'abc')
+
+ subject.execute([legacy_config])
+ end
+
+ it 'passes app_id and app_secret as additional arguments' do
+ twitter_config = { 'name' => 'twitter', 'app_id' => '123', 'app_secret' => 'abc' }
+
+ expect(devise_config).to receive(:omniauth).with(:twitter, '123', 'abc')
+
+ subject.execute([twitter_config])
+ end
+
+ it 'passes "args" hash as symbolized hash argument' do
+ hash_config = { 'name' => 'hash', 'args' => { 'custom' => 'format' } }
+
+ expect(devise_config).to receive(:omniauth).with(:hash, custom: 'format')
+
+ subject.execute([hash_config])
+ end
+
+ it 'configures fail_with_empty_uid for shibboleth' do
+ shibboleth_config = { 'name' => 'shibboleth', 'args' => {} }
+
+ expect(devise_config).to receive(:omniauth).with(:shibboleth, fail_with_empty_uid: true)
+
+ subject.execute([shibboleth_config])
+ end
+
+ it 'configures remote_sign_out_handler proc for authentiq' do
+ authentiq_config = { 'name' => 'authentiq', 'args' => {} }
+
+ expect(devise_config).to receive(:omniauth).with(:authentiq, remote_sign_out_handler: an_instance_of(Proc))
+
+ subject.execute([authentiq_config])
+ end
+
+ it 'configures on_single_sign_out proc for cas3' do
+ cas3_config = { 'name' => 'cas3', 'args' => {} }
+
+ expect(devise_config).to receive(:omniauth).with(:cas3, on_single_sign_out: an_instance_of(Proc))
+
+ subject.execute([cas3_config])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 3d5b56cd5b8..548eb28fe4d 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -110,8 +110,8 @@ describe Gitlab::Profiler do
custom_logger.debug('User Load (1.3ms)')
custom_logger.debug('Project Load (10.4ms)')
- expect(custom_logger.load_times_by_model).to eq('User' => 2.5,
- 'Project' => 10.4)
+ expect(custom_logger.load_times_by_model).to eq('User' => [1.2, 1.3],
+ 'Project' => [10.4])
end
it 'logs the backtrace, ignoring lines as appropriate' do
@@ -164,4 +164,24 @@ describe Gitlab::Profiler do
end
end
end
+
+ describe '.log_load_times_by_model' do
+ it 'logs the model, query count, and time by slowest first' do
+ expect(null_logger).to receive(:load_times_by_model).and_return(
+ 'User' => [1.2, 1.3],
+ 'Project' => [10.4]
+ )
+
+ expect(null_logger).to receive(:info).with('Project total (1): 10.4ms')
+ expect(null_logger).to receive(:info).with('User total (2): 2.5ms')
+
+ described_class.log_load_times_by_model(null_logger)
+ end
+
+ it 'does nothing when called with a logger that does not have load times' do
+ expect(null_logger).not_to receive(:info)
+
+ expect(described_class.log_load_times_by_model(null_logger)).to be_nil
+ end
+ end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 57905a74e92..8351b967133 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -83,19 +83,19 @@ describe Gitlab::ProjectSearchResults do
end
context 'when the matching filename contains a colon' do
- let(:search_result) { "\nmaster:testdata/project::function1.yaml\x001\x00---\n" }
+ let(:search_result) { "master:testdata/project::function1.yaml\x001\x00---\n" }
it 'returns a valid FoundBlob' do
expect(subject.filename).to eq('testdata/project::function1.yaml')
expect(subject.basename).to eq('testdata/project::function1')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
- expect(subject.data).to eq('---')
+ expect(subject.data).to eq("---\n")
end
end
context 'when the matching content contains a number surrounded by colons' do
- let(:search_result) { "\nmaster:testdata/foo.txt\x001\x00blah:9:blah" }
+ let(:search_result) { "master:testdata/foo.txt\x001\x00blah:9:blah" }
it 'returns a valid FoundBlob' do
expect(subject.filename).to eq('testdata/foo.txt')
@@ -106,6 +106,18 @@ describe Gitlab::ProjectSearchResults do
end
end
+ context 'when the search result ends with an empty line' do
+ let(:results) { project.repository.search_files_by_content('Role models', 'master') }
+
+ it 'returns a valid FoundBlob that ends with an empty line' do
+ expect(subject.filename).to eq('files/markdown/ruby-style-guide.md')
+ expect(subject.basename).to eq('files/markdown/ruby-style-guide')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("# Prelude\n\n> Role models are important. <br/>\n> -- Officer Alex J. Murphy / RoboCop\n\n")
+ end
+ end
+
context 'when the search returns non-ASCII data' do
context 'with UTF-8' do
let(:results) { project.repository.search_files_by_content('файл', 'master') }
@@ -115,7 +127,7 @@ describe Gitlab::ProjectSearchResults do
expect(subject.basename).to eq('encoding/russian')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
- expect(subject.data).to eq('Хороший файл')
+ expect(subject.data).to eq("Хороший файл\n")
end
end
@@ -139,7 +151,7 @@ describe Gitlab::ProjectSearchResults do
expect(subject.basename).to eq('encoding/iso8859')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
- expect(subject.data).to eq("Äü\n\nfoo")
+ expect(subject.data).to eq("Äü\n\nfoo\n")
end
end
end
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
index b67bcc77bd4..f030f371372 100644
--- a/spec/lib/gitlab/repo_path_spec.rb
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -48,8 +48,8 @@ describe ::Gitlab::RepoPath do
describe '.strip_storage_path' do
before do
allow(Gitlab.config.repositories).to receive(:storages).and_return({
- 'storage1' => { 'path' => '/foo' },
- 'storage2' => { 'path' => '/bar' }
+ 'storage1' => Gitlab::GitalyClient::StorageSettings.new('path' => '/foo'),
+ 'storage2' => Gitlab::GitalyClient::StorageSettings.new('path' => '/bar')
})
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 14b59c5e945..ea5ce58e34b 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -405,7 +405,7 @@ describe Gitlab::Shell do
describe '#create_repository' do
shared_examples '#create_repository' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
let(:repo_name) { 'project/path' }
let(:created_path) { File.join(repository_storage_path, repo_name + '.git') }
@@ -679,7 +679,7 @@ describe Gitlab::Shell do
describe 'namespace actions' do
subject { described_class.new }
- let(:storage_path) { Gitlab.config.repositories.storages.default.path }
+ let(:storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path }
describe '#add_namespace' do
it 'creates a namespace' do
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index d9b3c2350b1..2d35b026485 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
describe Gitlab::UrlBlocker do
describe '#blocked_url?' do
+ let(:valid_ports) { Project::VALID_IMPORT_PORTS }
+
it 'allows imports from configured web host and port' do
import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git"
expect(described_class.blocked_url?(import_url)).to be false
@@ -17,7 +19,7 @@ describe Gitlab::UrlBlocker do
end
it 'returns true for bad port' do
- expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git', valid_ports: valid_ports)).to be true
end
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
@@ -71,6 +73,47 @@ describe Gitlab::UrlBlocker do
it 'returns false for legitimate URL' do
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
end
+
+ context 'when allow_private_networks is' do
+ let(:private_networks) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] }
+ let(:fake_domain) { 'www.fakedomain.fake' }
+
+ context 'true (default)' do
+ it 'does not block urls from private networks' do
+ private_networks.each do |ip|
+ stub_domain_resolv(fake_domain, ip)
+
+ expect(described_class).not_to be_blocked_url("http://#{fake_domain}")
+
+ unstub_domain_resolv
+
+ expect(described_class).not_to be_blocked_url("http://#{ip}")
+ end
+ end
+ end
+
+ context 'false' do
+ it 'blocks urls from private networks' do
+ private_networks.each do |ip|
+ stub_domain_resolv(fake_domain, ip)
+
+ expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_private_networks: false)
+
+ unstub_domain_resolv
+
+ expect(described_class).to be_blocked_url("http://#{ip}", allow_private_networks: false)
+ end
+ end
+ end
+
+ def stub_domain_resolv(domain, ip)
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true)])
+ end
+
+ def unstub_domain_resolv
+ allow(Addrinfo).to receive(:getaddrinfo).and_call_original
+ end
+ end
end
# Resolv does not support resolving UTF-8 domain names
diff --git a/spec/lib/gitlab/verify/lfs_objects_spec.rb b/spec/lib/gitlab/verify/lfs_objects_spec.rb
index 64f3a9660e0..0f890e2c7ce 100644
--- a/spec/lib/gitlab/verify/lfs_objects_spec.rb
+++ b/spec/lib/gitlab/verify/lfs_objects_spec.rb
@@ -31,5 +31,21 @@ describe Gitlab::Verify::LfsObjects do
expect(failures.keys).to contain_exactly(lfs_object)
expect(failure.to_s).to include('Checksum mismatch')
end
+
+ context 'with remote files' do
+ before do
+ stub_lfs_object_storage
+ end
+
+ it 'skips LFS objects in object storage' do
+ local_failure = create(:lfs_object)
+ create(:lfs_object, :object_storage)
+
+ failures = {}
+ described_class.new(batch_size: 10).run_batches { |_, failed| failures.merge!(failed) }
+
+ expect(failures.keys).to contain_exactly(local_failure)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb
index 6146ce61226..85768308edc 100644
--- a/spec/lib/gitlab/verify/uploads_spec.rb
+++ b/spec/lib/gitlab/verify/uploads_spec.rb
@@ -40,5 +40,21 @@ describe Gitlab::Verify::Uploads do
expect(failures.keys).to contain_exactly(upload)
expect(failure.to_s).to include('Checksum missing')
end
+
+ context 'with remote files' do
+ before do
+ stub_uploads_object_storage(AvatarUploader)
+ end
+
+ it 'skips uploads in object storage' do
+ local_failure = create(:upload)
+ create(:upload, :object_storage)
+
+ failures = {}
+ described_class.new(batch_size: 10).run_batches { |_, failed| failures.merge!(failed) }
+
+ expect(failures.keys).to contain_exactly(local_failure)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 37a0bf1ad36..d67bfb37be7 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -55,7 +55,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_archive feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -100,7 +100,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_send_git_patch feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -173,7 +173,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_send_git_diff feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -275,14 +275,12 @@ describe Gitlab::Workhorse do
describe '.git_http_ok' do
let(:user) { create(:user) }
- let(:repo_path) { repository.path_to_repo }
let(:action) { 'info_refs' }
let(:params) do
{
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "project-#{project.id}",
- RepoPath: repo_path,
ShowAllRefs: false
}
end
@@ -297,7 +295,6 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "wiki-#{project.id}",
- RepoPath: repo_path,
ShowAllRefs: false
}
end
@@ -455,7 +452,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_raw_show feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb
index 369e7b181b9..8ba15ae0f38 100644
--- a/spec/lib/mattermost/command_spec.rb
+++ b/spec/lib/mattermost/command_spec.rb
@@ -4,10 +4,11 @@ describe Mattermost::Command do
let(:params) { { 'token' => 'token', team_id: 'abc' } }
before do
- Mattermost::Session.base_uri('http://mattermost.example.com')
+ session = Mattermost::Session.new(nil)
+ session.base_uri = 'http://mattermost.example.com'
allow_any_instance_of(Mattermost::Client).to receive(:with_session)
- .and_yield(Mattermost::Session.new(nil))
+ .and_yield(session)
end
describe '#create' do
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 3db19d06305..c855643c4d8 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -15,7 +15,7 @@ describe Mattermost::Session, type: :request do
it { is_expected.to respond_to(:strategy) }
before do
- described_class.base_uri(mattermost_url)
+ subject.base_uri = mattermost_url
end
describe '#with session' do
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
index 3c8206031cf..2cfa6802612 100644
--- a/spec/lib/mattermost/team_spec.rb
+++ b/spec/lib/mattermost/team_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
describe Mattermost::Team do
before do
- Mattermost::Session.base_uri('http://mattermost.example.com')
+ session = Mattermost::Session.new(nil)
+ session.base_uri = 'http://mattermost.example.com'
allow_any_instance_of(Mattermost::Client).to receive(:with_session)
- .and_yield(Mattermost::Session.new(nil))
+ .and_yield(session)
end
describe '#all' do
diff --git a/spec/migrations/remove_dot_git_from_usernames_spec.rb b/spec/migrations/remove_dot_git_from_usernames_spec.rb
index 129374cb38c..3a88a66a476 100644
--- a/spec/migrations/remove_dot_git_from_usernames_spec.rb
+++ b/spec/migrations/remove_dot_git_from_usernames_spec.rb
@@ -29,7 +29,9 @@ describe RemoveDotGitFromUsernames do
update_namespace(user, 'test.git')
update_namespace(user2, 'test_git')
- storages = { 'default' => 'tmp/tests/custom_repositories' }
+ default_hash = Gitlab.config.repositories.storages.default.to_h
+ default_hash['path'] = 'tmp/tests/custom_repositories'
+ storages = { 'default' => Gitlab::GitalyClient::StorageSettings.new(default_hash) }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
allow(migration).to receive(:route_exists?).with('test_git').and_return(true)
diff --git a/spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb b/spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb
new file mode 100644
index 00000000000..441c4295a40
--- /dev/null
+++ b/spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180220150310_remove_empty_extern_uid_auth0_identities.rb')
+
+describe RemoveEmptyExternUidAuth0Identities, :migration do
+ let(:identities) { table(:identities) }
+
+ before do
+ identities.create(provider: 'auth0', extern_uid: '')
+ identities.create(provider: 'auth0', extern_uid: 'valid')
+ identities.create(provider: 'github', extern_uid: '')
+
+ migrate!
+ end
+
+ it 'leaves the correct auth0 identity' do
+ expect(identities.where(provider: 'auth0').pluck(:extern_uid)).to eq(['valid'])
+ end
+
+ it 'leaves the correct github identity' do
+ expect(identities.where(provider: 'github').count).to eq(1)
+ end
+end
diff --git a/spec/migrations/remove_empty_fork_networks_spec.rb b/spec/migrations/remove_empty_fork_networks_spec.rb
index 7f7ce91378b..f6d030ab25c 100644
--- a/spec/migrations/remove_empty_fork_networks_spec.rb
+++ b/spec/migrations/remove_empty_fork_networks_spec.rb
@@ -19,6 +19,10 @@ describe RemoveEmptyForkNetworks, :migration do
deleted_project.destroy!
end
+ after do
+ Upload.reset_column_information
+ end
+
it 'deletes only the fork network without members' do
expect(fork_networks.count).to eq(2)
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 9da3de7a828..7d935cf8d76 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -198,6 +198,16 @@ describe Ci::Build do
end
context 'when legacy artifacts are used' do
+ let(:build) { create(:ci_build, :legacy_artifacts) }
+
+ subject { build.artifacts? }
+
+ context 'is expired' do
+ let(:build) { create(:ci_build, :legacy_artifacts, :expired) }
+
+ it { is_expected.to be_falsy }
+ end
+
context 'artifacts archive does not exist' do
let(:build) { create(:ci_build) }
@@ -208,13 +218,25 @@ describe Ci::Build do
let(:build) { create(:ci_build, :legacy_artifacts) }
it { is_expected.to be_truthy }
+ end
+ end
+ end
- context 'is expired' do
- let(:build) { create(:ci_build, :legacy_artifacts, :expired) }
+ describe '#browsable_artifacts?' do
+ subject { build.browsable_artifacts? }
- it { is_expected.to be_falsy }
- end
+ context 'artifacts metadata does not exist' do
+ before do
+ build.update_attributes(legacy_artifacts_metadata: nil)
end
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts metadata does exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+
+ it { is_expected.to be_truthy }
end
end
@@ -2101,6 +2123,35 @@ describe Ci::Build do
subject.drop!
end
+
+ context 'when retry service raises Gitlab::Access::AccessDeniedError exception' do
+ let(:retry_service) { Ci::RetryBuildService.new(subject.project, subject.user) }
+
+ before do
+ allow_any_instance_of(Ci::RetryBuildService)
+ .to receive(:execute)
+ .with(subject)
+ .and_raise(Gitlab::Access::AccessDeniedError)
+ allow(Rails.logger).to receive(:error)
+ end
+
+ it 'handles raised exception' do
+ expect { subject.drop! }.not_to raise_exception(Gitlab::Access::AccessDeniedError)
+ end
+
+ it 'logs the error' do
+ subject.drop!
+
+ expect(Rails.logger)
+ .to have_received(:error)
+ .with(a_string_matching("Unable to auto-retry job #{subject.id}"))
+ end
+
+ it 'fails the job' do
+ subject.drop!
+ expect(subject.failed?).to be_truthy
+ end
+ end
end
context 'when build is not configured to be retried' do
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index a2bd36537e6..1aa28434879 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -15,6 +15,50 @@ describe Ci::JobArtifact do
it { is_expected.to delegate_method(:open).to(:file) }
it { is_expected.to delegate_method(:exists?).to(:file) }
+ describe 'callbacks' do
+ subject { create(:ci_job_artifact, :archive) }
+
+ describe '#schedule_background_upload' do
+ context 'when object storage is disabled' do
+ before do
+ stub_artifacts_object_storage(enabled: false)
+ end
+
+ it 'does not schedule the migration' do
+ expect(ObjectStorageUploadWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ context 'when object storage is enabled' do
+ context 'when background upload is enabled' do
+ before do
+ stub_artifacts_object_storage(background_upload: true)
+ end
+
+ it 'schedules the model for migration' do
+ expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('JobArtifactUploader', described_class.name, :file, kind_of(Numeric))
+
+ subject
+ end
+ end
+
+ context 'when background upload is disabled' do
+ before do
+ stub_artifacts_object_storage(background_upload: false)
+ end
+
+ it 'schedules the model for migration' do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+ end
+ end
+
describe '#set_size' do
it 'sets the size' do
expect(artifact.size).to eq(106365)
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 4635f8cfe9d..92f00cfbc19 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -177,6 +177,24 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#protected_ref?' do
+ it 'delegates method to project' do
+ expect(pipeline).not_to be_protected_ref
+ end
+ end
+
+ describe '#legacy_trigger' do
+ let(:trigger_request) { create(:ci_trigger_request) }
+
+ before do
+ pipeline.trigger_requests << trigger_request
+ end
+
+ it 'returns first trigger request' do
+ expect(pipeline.legacy_trigger).to eq trigger_request
+ end
+ end
+
describe '#auto_canceled?' do
subject { pipeline.auto_canceled? }
@@ -215,142 +233,257 @@ describe Ci::Pipeline, :mailer do
end
describe 'pipeline stages' do
- before do
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'linux',
- stage_idx: 0,
- status: 'success')
-
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'mac',
- stage_idx: 0,
- status: 'failed')
-
- create(:commit_status, pipeline: pipeline,
- stage: 'deploy',
- name: 'staging',
- stage_idx: 2,
- status: 'running')
-
- create(:commit_status, pipeline: pipeline,
- stage: 'test',
- name: 'rspec',
- stage_idx: 1,
- status: 'success')
- end
-
describe '#stage_seeds' do
- let(:pipeline) do
- build(:ci_pipeline, config: { rspec: { script: 'rake' } })
- end
+ let(:pipeline) { build(:ci_pipeline, config: config) }
+ let(:config) { { rspec: { script: 'rake' } } }
it 'returns preseeded stage seeds object' do
- expect(pipeline.stage_seeds).to all(be_a Gitlab::Ci::Stage::Seed)
+ expect(pipeline.stage_seeds)
+ .to all(be_a Gitlab::Ci::Pipeline::Seed::Base)
expect(pipeline.stage_seeds.count).to eq 1
end
- end
- describe '#seeds_size' do
- let(:pipeline) { build(:ci_pipeline_with_one_job) }
+ context 'when no refs policy is specified' do
+ let(:config) do
+ { production: { stage: 'deploy', script: 'cap prod' },
+ rspec: { stage: 'test', script: 'rspec' },
+ spinach: { stage: 'test', script: 'spinach' } }
+ end
- it 'returns number of jobs in stage seeds' do
- expect(pipeline.seeds_size).to eq 1
+ it 'correctly fabricates a stage seeds object' do
+ seeds = pipeline.stage_seeds
+
+ expect(seeds.size).to eq 2
+ expect(seeds.first.attributes[:name]).to eq 'test'
+ expect(seeds.second.attributes[:name]).to eq 'deploy'
+ expect(seeds.dig(0, 0, :name)).to eq 'rspec'
+ expect(seeds.dig(0, 1, :name)).to eq 'spinach'
+ expect(seeds.dig(1, 0, :name)).to eq 'production'
+ end
end
- end
- describe '#legacy_stages' do
- subject { pipeline.legacy_stages }
+ context 'when refs policy is specified' do
+ let(:pipeline) do
+ build(:ci_pipeline, ref: 'feature', tag: true, config: config)
+ end
- context 'stages list' do
- it 'returns ordered list of stages' do
- expect(subject.map(&:name)).to eq(%w[build test deploy])
+ let(:config) do
+ { production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['tags'] } }
+ end
+
+ it 'returns stage seeds only assigned to master to master' do
+ seeds = pipeline.stage_seeds
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.attributes[:name]).to eq 'test'
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
end
end
- context 'stages with statuses' do
- let(:statuses) do
- subject.map { |stage| [stage.name, stage.status] }
+ context 'when source policy is specified' do
+ let(:pipeline) { build(:ci_pipeline, source: :schedule, config: config) }
+
+ let(:config) do
+ { production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['schedules'] } }
end
- it 'returns list of stages with correct statuses' do
- expect(statuses).to eq([%w(build failed),
- %w(test success),
- %w(deploy running)])
+ it 'returns stage seeds only assigned to schedules' do
+ seeds = pipeline.stage_seeds
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.attributes[:name]).to eq 'test'
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
end
+ end
- context 'when commit status is retried' do
- before do
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'mac',
- stage_idx: 0,
- status: 'success')
+ context 'when kubernetes policy is specified' do
+ let(:config) do
+ {
+ spinach: { stage: 'test', script: 'spinach' },
+ production: {
+ stage: 'deploy',
+ script: 'cap',
+ only: { kubernetes: 'active' }
+ }
+ }
+ end
+
+ context 'when kubernetes is active' do
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns seeds for kubernetes dependent job' do
+ seeds = pipeline.stage_seeds
- pipeline.process!
+ expect(seeds.size).to eq 2
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ expect(seeds.dig(1, 0, :name)).to eq 'production'
+ end
end
- it 'ignores the previous state' do
- expect(statuses).to eq([%w(build success),
- %w(test success),
- %w(deploy running)])
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+ let(:pipeline) { build(:ci_pipeline, project: project, config: config) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
- end
- end
- context 'when there is a stage with warnings' do
- before do
- create(:commit_status, pipeline: pipeline,
- stage: 'deploy',
- name: 'prod:2',
- stage_idx: 2,
- status: 'failed',
- allow_failure: true)
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let(:pipeline) { build(:ci_pipeline, project: project, config: config) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
end
- it 'populates stage with correct number of warnings' do
- deploy_stage = pipeline.legacy_stages.third
+ context 'when kubernetes is not active' do
+ it 'does not return seeds for kubernetes dependent job' do
+ seeds = pipeline.stage_seeds
- expect(deploy_stage).not_to receive(:statuses)
- expect(deploy_stage).to have_warnings
+ expect(seeds.size).to eq 1
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ end
end
end
end
- describe '#stages_count' do
- it 'returns a valid number of stages' do
- expect(pipeline.stages_count).to eq(3)
- end
- end
+ describe '#seeds_size' do
+ context 'when refs policy is specified' do
+ let(:config) do
+ { production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['tags'] } }
+ end
- describe '#stages_names' do
- it 'returns a valid names of stages' do
- expect(pipeline.stages_names).to eq(%w(build test deploy))
+ let(:pipeline) do
+ build(:ci_pipeline, ref: 'feature', tag: true, config: config)
+ end
+
+ it 'returns real seeds size' do
+ expect(pipeline.seeds_size).to eq 1
+ end
end
end
- end
-
- describe '#legacy_stage' do
- subject { pipeline.legacy_stage('test') }
- context 'with status in stage' do
+ describe 'legacy stages' do
before do
- create(:commit_status, pipeline: pipeline, stage: 'test')
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'linux',
+ stage_idx: 0,
+ status: 'success')
+
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'failed')
+
+ create(:commit_status, pipeline: pipeline,
+ stage: 'deploy',
+ name: 'staging',
+ stage_idx: 2,
+ status: 'running')
+
+ create(:commit_status, pipeline: pipeline,
+ stage: 'test',
+ name: 'rspec',
+ stage_idx: 1,
+ status: 'success')
+ end
+
+ describe '#legacy_stages' do
+ subject { pipeline.legacy_stages }
+
+ context 'stages list' do
+ it 'returns ordered list of stages' do
+ expect(subject.map(&:name)).to eq(%w[build test deploy])
+ end
+ end
+
+ context 'stages with statuses' do
+ let(:statuses) do
+ subject.map { |stage| [stage.name, stage.status] }
+ end
+
+ it 'returns list of stages with correct statuses' do
+ expect(statuses).to eq([%w(build failed),
+ %w(test success),
+ %w(deploy running)])
+ end
+
+ context 'when commit status is retried' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'success')
+
+ pipeline.process!
+ end
+
+ it 'ignores the previous state' do
+ expect(statuses).to eq([%w(build success),
+ %w(test success),
+ %w(deploy running)])
+ end
+ end
+ end
+
+ context 'when there is a stage with warnings' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'deploy',
+ name: 'prod:2',
+ stage_idx: 2,
+ status: 'failed',
+ allow_failure: true)
+ end
+
+ it 'populates stage with correct number of warnings' do
+ deploy_stage = pipeline.legacy_stages.third
+
+ expect(deploy_stage).not_to receive(:statuses)
+ expect(deploy_stage).to have_warnings
+ end
+ end
+ end
+
+ describe '#stages_count' do
+ it 'returns a valid number of stages' do
+ expect(pipeline.stages_count).to eq(3)
+ end
end
- it { expect(subject).to be_a Ci::LegacyStage }
- it { expect(subject.name).to eq 'test' }
- it { expect(subject.statuses).not_to be_empty }
+ describe '#stages_names' do
+ it 'returns a valid names of stages' do
+ expect(pipeline.stages_names).to eq(%w(build test deploy))
+ end
+ end
end
- context 'without status in stage' do
- before do
- create(:commit_status, pipeline: pipeline, stage: 'build')
+ describe '#legacy_stage' do
+ subject { pipeline.legacy_stage('test') }
+
+ context 'with status in stage' do
+ before do
+ create(:commit_status, pipeline: pipeline, stage: 'test')
+ end
+
+ it { expect(subject).to be_a Ci::LegacyStage }
+ it { expect(subject.name).to eq 'test' }
+ it { expect(subject.statuses).not_to be_empty }
end
- it 'return stage object' do
- is_expected.to be_nil
+ context 'without status in stage' do
+ before do
+ create(:commit_status, pipeline: pipeline, stage: 'build')
+ end
+
+ it 'return stage object' do
+ is_expected.to be_nil
+ end
end
end
end
@@ -589,20 +722,6 @@ describe Ci::Pipeline, :mailer do
end
end
- describe '#has_stage_seeds?' do
- context 'when pipeline has stage seeds' do
- subject { build(:ci_pipeline_with_one_job) }
-
- it { is_expected.to have_stage_seeds }
- end
-
- context 'when pipeline does not have stage seeds' do
- subject { create(:ci_pipeline_without_jobs) }
-
- it { is_expected.not_to have_stage_seeds }
- end
- end
-
describe '#has_warnings?' do
subject { pipeline.has_warnings? }
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4b217df2e8f..f8874d14e3f 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -34,7 +34,7 @@ describe Issuable do
subject { build(:issue) }
before do
- allow(subject).to receive(:set_iid).and_return(false)
+ allow(InternalId).to receive(:generate_next).and_return(nil)
end
it { is_expected.to validate_presence_of(:project) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index abfc0896a41..d620943693c 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -240,7 +240,7 @@ describe Group do
it "is false if avatar is html page" do
group.update_attribute(:avatar, 'uploads/avatar.html')
- expect(group.avatar_type).to eq(["only images allowed"])
+ expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff"])
end
end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
new file mode 100644
index 00000000000..581fd0293cc
--- /dev/null
+++ b/spec/models/internal_id_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe InternalId do
+ let(:project) { create(:project) }
+ let(:usage) { :issues }
+ let(:issue) { build(:issue, project: project) }
+ let(:scope) { { project: project } }
+ let(:init) { ->(s) { s.project.issues.size } }
+
+ context 'validations' do
+ it { is_expected.to validate_presence_of(:usage) }
+ end
+
+ describe '.generate_next' do
+ subject { described_class.generate_next(issue, scope, usage, init) }
+
+ context 'in the absence of a record' do
+ it 'creates a record if not yet present' do
+ expect { subject }.to change { described_class.count }.from(0).to(1)
+ end
+
+ it 'stores record attributes' do
+ subject
+
+ described_class.first.tap do |record|
+ expect(record.project).to eq(project)
+ expect(record.usage).to eq(usage.to_s)
+ end
+ end
+
+ context 'with existing issues' do
+ before do
+ rand(1..10).times { create(:issue, project: project) }
+ described_class.delete_all
+ end
+
+ it 'calculates last_value values automatically' do
+ expect(subject).to eq(project.issues.size + 1)
+ end
+ end
+
+ context 'with concurrent inserts on table' do
+ it 'looks up the record if it was created concurrently' do
+ args = { **scope, usage: described_class.usages[usage.to_s] }
+ record = double
+ expect(described_class).to receive(:find_by).with(args).and_return(nil) # first call, record not present
+ expect(described_class).to receive(:find_by).with(args).and_return(record) # second call, record was created by another process
+ expect(described_class).to receive(:create!).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
+ expect(record).to receive(:increment_and_save!)
+
+ subject
+ end
+ end
+ end
+
+ it 'generates a strictly monotone, gapless sequence' do
+ seq = (0..rand(100)).map do
+ described_class.generate_next(issue, scope, usage, init)
+ end
+ normalized = seq.map { |i| i - seq.min }
+
+ expect(normalized).to eq((0..seq.size - 1).to_a)
+ end
+
+ context 'with an insufficient schema version' do
+ before do
+ described_class.reset_column_information
+ expect(ActiveRecord::Migrator).to receive(:current_version).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
+ end
+
+ let(:init) { double('block') }
+
+ it 'calculates next internal ids on the fly' do
+ val = rand(1..100)
+
+ expect(init).to receive(:call).with(issue).and_return(val)
+ expect(subject).to eq(val + 1)
+ end
+ end
+ end
+
+ describe '#increment_and_save!' do
+ let(:id) { create(:internal_id) }
+ subject { id.increment_and_save! }
+
+ it 'returns incremented iid' do
+ value = id.last_value
+
+ expect(subject).to eq(value + 1)
+ end
+
+ it 'saves the record' do
+ subject
+
+ expect(id.changed?).to be_falsey
+ end
+
+ context 'with last_value=nil' do
+ let(:id) { build(:internal_id, last_value: nil) }
+
+ it 'returns 1' do
+ expect(subject).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index feed7968f09..11154291368 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -9,11 +9,17 @@ describe Issue do
describe 'modules' do
subject { described_class }
- it { is_expected.to include_module(InternalId) }
it { is_expected.to include_module(Issuable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:issue) }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :issues }
+ end
end
subject { create(:issue) }
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
new file mode 100644
index 00000000000..a182116d637
--- /dev/null
+++ b/spec/models/lfs_object_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe LfsObject do
+ describe '#local_store?' do
+ it 'returns true when file_store is nil' do
+ subject.file_store = nil
+
+ expect(subject.local_store?).to eq true
+ end
+
+ it 'returns true when file_store is equal to LfsObjectUploader::Store::LOCAL' do
+ subject.file_store = LfsObjectUploader::Store::LOCAL
+
+ expect(subject.local_store?).to eq true
+ end
+
+ it 'returns false whe file_store is equal to LfsObjectUploader::Store::REMOTE' do
+ subject.file_store = LfsObjectUploader::Store::REMOTE
+
+ expect(subject.local_store?).to eq false
+ end
+ end
+
+ describe '#schedule_background_upload' do
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ subject { create(:lfs_object, :with_file) }
+
+ context 'when object storage is disabled' do
+ before do
+ stub_lfs_object_storage(enabled: false)
+ end
+
+ it 'does not schedule the migration' do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ context 'when object storage is enabled' do
+ context 'when background upload is enabled' do
+ context 'when is licensed' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
+
+ it 'schedules the model for migration' do
+ expect(ObjectStorage::BackgroundMoveWorker)
+ .to receive(:perform_async)
+ .with('LfsObjectUploader', described_class.name, :file, kind_of(Numeric))
+ .once
+
+ subject
+ end
+
+ it 'schedules the model for migration once' do
+ expect(ObjectStorage::BackgroundMoveWorker)
+ .to receive(:perform_async)
+ .with('LfsObjectUploader', described_class.name, :file, kind_of(Numeric))
+ .once
+
+ lfs_object = create(:lfs_object)
+ lfs_object.file = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png")
+ lfs_object.save!
+ end
+ end
+ end
+
+ context 'when background upload is disabled' do
+ before do
+ stub_lfs_object_storage(background_upload: false)
+ end
+
+ it 'schedules the model for migration' do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 4e783acbd8b..f73f44ca0ad 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -17,7 +17,7 @@ describe MergeRequest do
describe 'modules' do
subject { described_class }
- it { is_expected.to include_module(InternalId) }
+ it { is_expected.to include_module(NonatomicInternalId) }
it { is_expected.to include_module(Issuable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
@@ -1961,6 +1961,17 @@ describe MergeRequest do
expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3)
end
end
+
+ it 'runs a single query on the initial call, and none afterwards' do
+ expect { subject.merge_request_diff_for(merge_request_diff1.diff_refs) }
+ .not_to exceed_query_limit(1)
+
+ expect { subject.merge_request_diff_for(merge_request_diff2.diff_refs) }
+ .not_to exceed_query_limit(0)
+
+ expect { subject.merge_request_diff_for(merge_request_diff3.head_commit_sha) }
+ .not_to exceed_query_limit(0)
+ end
end
describe '#version_params_for' do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index ee142718f7e..62e95a622eb 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -305,7 +305,7 @@ describe Namespace do
end
describe '#rm_dir', 'callback' do
- let(:repository_storage_path) { Gitlab.config.repositories.storages.default['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path }
let(:path_in_dir) { File.join(repository_storage_path, namespace.full_path) }
let(:deleted_path) { namespace.full_path.gsub(namespace.path, "#{namespace.full_path}+#{namespace.id}+deleted") }
let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) }
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 95713d8b85b..4b85c5e8720 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -18,24 +18,63 @@ describe PagesDomain do
it { is_expected.to validate_uniqueness_of(:domain).case_insensitive }
end
- {
- 'my.domain.com' => true,
- '123.456.789' => true,
- '0x12345.com' => true,
- '0123123' => true,
- '_foo.com' => false,
- 'reserved.com' => false,
- 'a.reserved.com' => false,
- nil => false
- }.each do |value, validity|
- context "domain #{value.inspect} validity" do
- before do
- allow(Settings.pages).to receive(:host).and_return('reserved.com')
+ describe "hostname" do
+ {
+ 'my.domain.com' => true,
+ '123.456.789' => true,
+ '0x12345.com' => true,
+ '0123123' => true,
+ '_foo.com' => false,
+ 'reserved.com' => false,
+ 'a.reserved.com' => false,
+ nil => false
+ }.each do |value, validity|
+ context "domain #{value.inspect} validity" do
+ before do
+ allow(Settings.pages).to receive(:host).and_return('reserved.com')
+ end
+
+ let(:domain) { value }
+
+ it { expect(pages_domain.valid?).to eq(validity) }
+ end
+ end
+ end
+
+ describe "HTTPS-only" do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:domain) { 'my.domain.com' }
+
+ let(:project) do
+ instance_double(Project, pages_https_only?: pages_https_only)
+ end
+
+ let(:pages_domain) do
+ build(:pages_domain, certificate: certificate, key: key).tap do |pd|
+ allow(pd).to receive(:project).and_return(project)
+ pd.valid?
end
+ end
- let(:domain) { value }
+ where(:pages_https_only, :certificate, :key, :errors_on) do
+ attributes = attributes_for(:pages_domain)
+ cert, key = attributes.fetch_values(:certificate, :key)
+
+ true | nil | nil | %i(certificate key)
+ true | cert | nil | %i(key)
+ true | nil | key | %i(certificate key)
+ true | cert | key | []
+ false | nil | nil | []
+ false | cert | nil | %i(key)
+ false | nil | key | %i(key)
+ false | cert | key | []
+ end
- it { expect(pages_domain.valid?).to eq(validity) }
+ with_them do
+ it "is adds the expected errors" do
+ expect(pages_domain.errors.keys).to eq errors_on
+ end
end
end
end
@@ -43,26 +82,26 @@ describe PagesDomain do
describe 'validate certificate' do
subject { domain }
- context 'when only certificate is specified' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ context 'with matching key' do
+ let(:domain) { build(:pages_domain) }
- it { is_expected.not_to be_valid }
+ it { is_expected.to be_valid }
end
- context 'when only key is specified' do
- let(:domain) { build(:pages_domain, :with_key) }
+ context 'when no certificate is specified' do
+ let(:domain) { build(:pages_domain, :without_certificate) }
it { is_expected.not_to be_valid }
end
- context 'with matching key' do
- let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+ context 'when no key is specified' do
+ let(:domain) { build(:pages_domain, :without_key) }
- it { is_expected.to be_valid }
+ it { is_expected.not_to be_valid }
end
context 'for not matching key' do
- let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+ let(:domain) { build(:pages_domain, :with_missing_chain) }
it { is_expected.not_to be_valid }
end
@@ -103,30 +142,26 @@ describe PagesDomain do
describe '#url' do
subject { domain.url }
- context 'without the certificate' do
- let(:domain) { build(:pages_domain, certificate: '') }
+ let(:domain) { build(:pages_domain) }
- it { is_expected.to eq("http://#{domain.domain}") }
- end
+ it { is_expected.to eq("https://#{domain.domain}") }
- context 'with a certificate' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ context 'without the certificate' do
+ let(:domain) { build(:pages_domain, :without_certificate) }
- it { is_expected.to eq("https://#{domain.domain}") }
+ it { is_expected.to eq("http://#{domain.domain}") }
end
end
describe '#has_matching_key?' do
subject { domain.has_matching_key? }
- context 'for matching key' do
- let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+ let(:domain) { build(:pages_domain) }
- it { is_expected.to be_truthy }
- end
+ it { is_expected.to be_truthy }
context 'for invalid key' do
- let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+ let(:domain) { build(:pages_domain, :with_missing_chain) }
it { is_expected.to be_falsey }
end
@@ -136,7 +171,7 @@ describe PagesDomain do
subject { domain.has_intermediates? }
context 'for self signed' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ let(:domain) { build(:pages_domain) }
it { is_expected.to be_truthy }
end
@@ -162,7 +197,7 @@ describe PagesDomain do
subject { domain.expired? }
context 'for valid' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ let(:domain) { build(:pages_domain) }
it { is_expected.to be_falsey }
end
@@ -175,7 +210,7 @@ describe PagesDomain do
end
describe '#subject' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ let(:domain) { build(:pages_domain) }
subject { domain.subject }
@@ -183,7 +218,7 @@ describe PagesDomain do
end
describe '#certificate_text' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ let(:domain) { build(:pages_domain) }
subject { domain.certificate_text }
@@ -191,6 +226,18 @@ describe PagesDomain do
it { is_expected.not_to be_empty }
end
+ describe "#https?" do
+ context "when a certificate is present" do
+ subject { build(:pages_domain) }
+ it { is_expected.to be_https }
+ end
+
+ context "when no certificate is present" do
+ subject { build(:pages_domain, :without_certificate) }
+ it { is_expected.not_to be_https }
+ end
+ end
+
describe '#update_daemon' do
it 'runs when the domain is created' do
domain = build(:pages_domain)
@@ -267,29 +314,30 @@ describe PagesDomain do
end
context 'TLS configuration' do
- set(:domain_with_tls) { create(:pages_domain, :with_key, :with_certificate) }
+ set(:domain_without_tls) { create(:pages_domain, :without_certificate, :without_key) }
+ set(:domain) { create(:pages_domain) }
- let(:cert1) { domain_with_tls.certificate }
+ let(:cert1) { domain.certificate }
let(:cert2) { cert1 + ' ' }
- let(:key1) { domain_with_tls.key }
+ let(:key1) { domain.key }
let(:key2) { key1 + ' ' }
it 'updates when added' do
- expect(domain).to receive(:update_daemon)
+ expect(domain_without_tls).to receive(:update_daemon)
- domain.update!(key: key1, certificate: cert1)
+ domain_without_tls.update!(key: key1, certificate: cert1)
end
it 'updates when changed' do
- expect(domain_with_tls).to receive(:update_daemon)
+ expect(domain).to receive(:update_daemon)
- domain_with_tls.update!(key: key2, certificate: cert2)
+ domain.update!(key: key2, certificate: cert2)
end
it 'updates when removed' do
- expect(domain_with_tls).to receive(:update_daemon)
+ expect(domain).to receive(:update_daemon)
- domain_with_tls.update!(key: nil, certificate: nil)
+ domain.update!(key: nil, certificate: nil)
end
end
end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index a5bdf9a9337..05d33cd3874 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -9,10 +9,11 @@ describe MattermostSlashCommandsService do
let(:user) { create(:user) }
before do
- Mattermost::Session.base_uri("http://mattermost.example.com")
+ session = Mattermost::Session.new(nil)
+ session.base_uri = 'http://mattermost.example.com'
allow_any_instance_of(Mattermost::Client).to receive(:with_session)
- .and_yield(Mattermost::Session.new(nil))
+ .and_yield(session)
end
describe '#configure' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4cf8d861595..1a7a6e035ea 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -922,7 +922,7 @@ describe Project do
it 'is false if avatar is html page' do
project.update_attribute(:avatar, 'uploads/avatar.html')
- expect(project.avatar_type).to eq(['only images allowed'])
+ expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff'])
end
end
@@ -1101,8 +1101,8 @@ describe Project do
before do
storages = {
- 'default' => { 'path' => 'tmp/tests/repositories' },
- 'picked' => { 'path' => 'tmp/tests/repositories' }
+ 'default' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories'),
+ 'picked' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories')
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
@@ -3479,4 +3479,49 @@ describe Project do
end
end
end
+
+ describe "#pages_https_only?" do
+ subject { build(:project) }
+
+ context "when HTTPS pages are disabled" do
+ it { is_expected.not_to be_pages_https_only }
+ end
+
+ context "when HTTPS pages are enabled", :https_pages_enabled do
+ it { is_expected.to be_pages_https_only }
+ end
+ end
+
+ describe "#pages_https_only? validation", :https_pages_enabled do
+ subject(:project) do
+ # set-up dirty object:
+ create(:project, pages_https_only: false).tap do |p|
+ p.pages_https_only = true
+ end
+ end
+
+ context "when no domains are associated" do
+ it { is_expected.to be_valid }
+ end
+
+ context "when domains including keys and certificates are associated" do
+ before do
+ allow(project)
+ .to receive(:pages_domains)
+ .and_return([instance_double(PagesDomain, https?: true)])
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context "when domains including no keys or certificates are associated" do
+ before do
+ allow(project)
+ .to receive(:pages_domains)
+ .and_return([instance_double(PagesDomain, https?: false)])
+ end
+
+ it { is_expected.not_to be_valid }
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 5bc972bca14..60ab52565cb 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -501,28 +501,6 @@ describe Repository do
end
end
- describe '#create_hooks' do
- let(:hook_path) { File.join(repository.path_to_repo, 'hooks') }
-
- it 'symlinks the global hooks directory' do
- repository.create_hooks
-
- expect(File.symlink?(hook_path)).to be true
- expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
- end
-
- it 'replaces existing symlink with the right directory' do
- FileUtils.mkdir_p(hook_path)
-
- expect(File.symlink?(hook_path)).to be false
-
- repository.create_hooks
-
- expect(File.symlink?(hook_path)).to be true
- expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
- end
- end
-
describe "#create_dir" do
it "commits a change that creates a new directory" do
expect do
@@ -895,7 +873,7 @@ describe Repository do
end
it 'returns nil when the content is not recognizable' do
- repository.create_file(user, 'LICENSE', 'Copyright!',
+ repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to be_nil
@@ -939,7 +917,7 @@ describe Repository do
end
it 'returns nil when the content is not recognizable' do
- repository.create_file(user, 'LICENSE', 'Copyright!',
+ repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license).to be_nil
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index dfac82b327a..01238a89a81 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -16,66 +16,6 @@ describe Route do
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path).case_insensitive }
-
- describe '#ensure_permanent_paths' do
- context 'when the route is not yet persisted' do
- let(:new_route) { described_class.new(path: 'foo', source: build(:group)) }
-
- context 'when permanent conflicting redirects exist' do
- it 'is invalid' do
- redirect = build(:redirect_route, :permanent, path: 'foo/bar/baz')
- redirect.save!(validate: false)
-
- expect(new_route.valid?).to be_falsey
- expect(new_route.errors.first[1]).to eq('has been taken before')
- end
- end
-
- context 'when no permanent conflicting redirects exist' do
- it 'is valid' do
- expect(new_route.valid?).to be_truthy
- end
- end
- end
-
- context 'when path has changed' do
- before do
- route.path = 'foo'
- end
-
- context 'when permanent conflicting redirects exist' do
- it 'is invalid' do
- redirect = build(:redirect_route, :permanent, path: 'foo/bar/baz')
- redirect.save!(validate: false)
-
- expect(route.valid?).to be_falsey
- expect(route.errors.first[1]).to eq('has been taken before')
- end
- end
-
- context 'when no permanent conflicting redirects exist' do
- it 'is valid' do
- expect(route.valid?).to be_truthy
- end
- end
- end
-
- context 'when path has not changed' do
- context 'when permanent conflicting redirects exist' do
- it 'is valid' do
- redirect = build(:redirect_route, :permanent, path: 'git_lab/foo/bar')
- redirect.save!(validate: false)
-
- expect(route.valid?).to be_truthy
- end
- end
- context 'when no permanent conflicting redirects exist' do
- it 'is valid' do
- expect(route.valid?).to be_truthy
- end
- end
- end
- end
end
describe 'callbacks' do
@@ -211,43 +151,31 @@ describe Route do
end
context 'when the source is a Project' do
- it 'creates a temporal RedirectRoute' do
+ it 'creates a RedirectRoute' do
project = create(:project)
route = project.route
redirect_route = route.create_redirect('foo')
- expect(redirect_route.permanent?).to be_falsy
+ expect(redirect_route).not_to be_nil
end
end
context 'when the source is not a project' do
- it 'creates a permanent RedirectRoute' do
- redirect_route = route.create_redirect('foo', permanent: true)
- expect(redirect_route.permanent?).to be_truthy
+ it 'creates a RedirectRoute' do
+ redirect_route = route.create_redirect('foo')
+ expect(redirect_route).not_to be_nil
end
end
end
describe '#delete_conflicting_redirects' do
- context 'with permanent redirect' do
- it 'does not delete the redirect' do
- route.create_redirect("#{route.path}/foo", permanent: true)
-
- expect do
- route.delete_conflicting_redirects
- end.not_to change { RedirectRoute.count }
- end
- end
-
- context 'with temporal redirect' do
- let(:route) { create(:project).route }
+ let(:route) { create(:project).route }
- it 'deletes the redirect' do
- route.create_redirect("#{route.path}/foo")
+ it 'deletes the redirect' do
+ route.create_redirect("#{route.path}/foo")
- expect do
- route.delete_conflicting_redirects
- end.to change { RedirectRoute.count }.by(-1)
- end
+ expect do
+ route.delete_conflicting_redirects
+ end.to change { RedirectRoute.count }.by(-1)
end
context 'when a redirect route with the same path exists' do
@@ -289,31 +217,18 @@ describe Route do
end
describe '#conflicting_redirects' do
+ let(:route) { create(:project).route }
+
it 'returns an ActiveRecord::Relation' do
expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
end
- context 'with permanent redirects' do
- it 'does not return anything' do
- route.create_redirect("#{route.path}/foo", permanent: true)
- route.create_redirect("#{route.path}/foo/bar", permanent: true)
- route.create_redirect("#{route.path}/baz/quz", permanent: true)
+ it 'returns the redirect routes' do
+ redirect1 = route.create_redirect("#{route.path}/foo")
+ redirect2 = route.create_redirect("#{route.path}/foo/bar")
+ redirect3 = route.create_redirect("#{route.path}/baz/quz")
- expect(route.conflicting_redirects).to be_empty
- end
- end
-
- context 'with temporal redirects' do
- let(:route) { create(:project).route }
-
- it 'returns the redirect routes' do
- route = create(:project).route
- redirect1 = route.create_redirect("#{route.path}/foo")
- redirect2 = route.create_redirect("#{route.path}/foo/bar")
- redirect3 = route.create_redirect("#{route.path}/baz/quz")
-
- expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3])
- end
+ expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3])
end
context 'when a redirect route with the same path exists' do
@@ -348,44 +263,6 @@ describe Route do
end
end
- describe "#conflicting_redirect_exists?" do
- context 'when a conflicting redirect exists' do
- let(:group1) { create(:group, path: 'foo') }
- let(:group2) { create(:group, path: 'baz') }
-
- it 'should not be saved' do
- group1.path = 'bar'
- group1.save
-
- group2.path = 'foo'
-
- expect(group2.save).to be_falsy
- end
-
- it 'should return an error on path' do
- group1.path = 'bar'
- group1.save
-
- group2.path = 'foo'
- group2.valid?
- expect(group2.errors[:path]).to eq(['has been taken before'])
- end
- end
-
- context 'when a conflicting redirect does not exist' do
- let(:project1) { create(:project, path: 'foo') }
- let(:project2) { create(:project, path: 'baz') }
-
- it 'should be saved' do
- project1.path = 'bar'
- project1.save
-
- project2.path = 'foo'
- expect(project2.save).to be_truthy
- end
- end
- end
-
describe '#delete_conflicting_orphaned_routes' do
context 'when there is a conflicting route' do
let!(:conflicting_group) { create(:group, path: 'foo') }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 5680eb24985..bbfdda23a31 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -126,23 +126,6 @@ describe User do
end
end
- context 'when the username was used by another user before' do
- let(:username) { 'foo' }
- let!(:other_user) { create(:user, username: username) }
-
- before do
- other_user.username = 'bar'
- other_user.save!
- end
-
- it 'is invalid' do
- user = build(:user, username: username)
-
- expect(user).not_to be_valid
- expect(user.errors.full_messages).to eq(['Username has been taken before'])
- end
- end
-
context 'when the username is in use by another user' do
let(:username) { 'foo' }
let!(:other_user) { create(:user, username: username) }
@@ -1222,7 +1205,7 @@ describe User do
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
- expect(user.avatar_type).to eq(['only images allowed'])
+ expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff'])
end
end
@@ -2699,27 +2682,19 @@ describe User do
end
end
- describe "#username_previously_taken?" do
- let(:user1) { create(:user, username: 'foo') }
+ context 'changing a username' do
+ let(:user) { create(:user, username: 'foo') }
- context 'when the username has been taken before' do
- before do
- user1.username = 'bar'
- user1.save!
- end
-
- it 'should raise an ActiveRecord::RecordInvalid exception' do
- user2 = build(:user, username: 'foo')
- expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Username has been taken before/)
- end
+ it 'creates a redirect route' do
+ expect { user.update!(username: 'bar') }
+ .to change { RedirectRoute.where(path: 'foo').count }.by(1)
end
- context 'when the username has not been taken before' do
- it 'should be valid' do
- expect(RedirectRoute.count).to eq(0)
- user2 = build(:user, username: 'baz')
- expect(user2).to be_valid
- end
+ it 'deletes the redirect when a user with the old username was created' do
+ user.update!(username: 'bar')
+
+ expect { create(:user, username: 'foo') }
+ .to change { RedirectRoute.where(path: 'foo').count }.by(-1)
end
end
end
diff --git a/spec/policies/protected_branch_policy_spec.rb b/spec/policies/protected_branch_policy_spec.rb
new file mode 100644
index 00000000000..b39de42d721
--- /dev/null
+++ b/spec/policies/protected_branch_policy_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe ProtectedBranchPolicy do
+ let(:user) { create(:user) }
+ let(:name) { 'feature' }
+ let(:protected_branch) { create(:protected_branch, name: name) }
+ let(:project) { protected_branch.project }
+
+ subject { described_class.new(user, protected_branch) }
+
+ it 'branches can be updated via project masters' do
+ project.add_master(user)
+
+ is_expected.to be_allowed(:update_protected_branch)
+ end
+
+ it "branches can't be updated by guests" do
+ project.add_guest(user)
+
+ is_expected.to be_disallowed(:update_protected_branch)
+ end
+end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index f8c93d91ec5..55962f345d4 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -339,7 +339,7 @@ describe ProjectPresenter do
it 'returns link to clusters page if more than one exists' do
project.add_master(user)
- create(:cluster, projects: [project])
+ create(:cluster, :production_environment, projects: [project])
create(:cluster, projects: [project])
expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: true,
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 852f67db958..8ad19e3f0f5 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1141,4 +1141,33 @@ describe API::Commits do
end
end
end
+
+ describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do
+ let!(:project) { create(:project, :repository, :private) }
+ let!(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
+ let(:commit) { merged_mr.merge_request_diff.commits.last }
+
+ it 'returns the correct merge request' do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(1)
+ expect(json_response[0]['id']).to eq(merged_mr.id)
+ end
+
+ it 'returns 403 for an unauthorized user' do
+ project.add_guest(user)
+
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'responds 404 when the commit does not exist' do
+ get api("/projects/#{project.id}/repository/commits/a7d26f00c35b/merge_requests", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index ca0aac87ba9..3cb90a1b8ef 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -447,6 +447,12 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
+ expect(json_response["gitaly"]).not_to be_nil
+ expect(json_response["gitaly"]["repository"]).not_to be_nil
+ expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name)
+ expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
+ expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
+ expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
end
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 6192bbd4abb..3ffdfdc0e9a 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe API::Jobs do
+ include HttpIOHelpers
+
set(:project) do
create(:project, :repository, public_builds: false)
end
@@ -112,6 +114,7 @@ describe API::Jobs do
let(:query) { Hash.new }
before do
+ job
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
end
@@ -335,10 +338,55 @@ describe API::Jobs do
end
end
+ context 'when artifacts are stored remotely' do
+ let(:proxy_download) { false }
+
+ before do
+ stub_artifacts_object_storage(proxy_download: proxy_download)
+ end
+
+ let(:job) { create(:ci_build, pipeline: pipeline) }
+ let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
+
+ before do
+ job.reload
+
+ get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
+ end
+
+ context 'when proxy download is enabled' do
+ let(:proxy_download) { true }
+
+ it 'responds with the workhorse send-url' do
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
+ end
+ end
+
+ context 'when proxy download is disabled' do
+ it 'returns location redirect' do
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'authorized user' do
+ it 'returns the file remote URL' do
+ expect(response).to redirect_to(artifact.file.url)
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job artifacts' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
it 'does not return job artifacts if not uploaded' do
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -349,6 +397,7 @@ describe API::Jobs do
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
before do
+ stub_artifacts_object_storage
job.success
end
@@ -412,9 +461,24 @@ describe API::Jobs do
"attachment; filename=#{job.artifacts_file.filename}" }
end
- it { expect(response).to have_gitlab_http_status(200) }
+ it { expect(response).to have_http_status(:ok) }
it { expect(response.headers).to include(download_headers) }
end
+
+ context 'when artifacts are stored remotely' do
+ let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) }
+ let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
+
+ before do
+ job.reload
+
+ get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
+ end
+
+ it 'returns location redirect' do
+ expect(response).to have_http_status(:found)
+ end
+ end
end
context 'with regular branch' do
@@ -451,6 +515,22 @@ describe API::Jobs do
end
context 'authorized user' do
+ context 'when trace is in ObjectStorage' do
+ let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+ before do
+ stub_remote_trace_206
+ allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
+ allow_any_instance_of(JobArtifactUploader).to receive(:url) { remote_trace_url }
+ allow_any_instance_of(JobArtifactUploader).to receive(:size) { remote_trace_size }
+ end
+
+ it 'returns specific job trace' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to eq(job.trace.raw)
+ end
+ end
+
context 'when trace is artifact' do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index dc3a116c060..a9ccbb32666 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -1,17 +1,17 @@
require 'rails_helper'
describe API::PagesDomains do
- set(:project) { create(:project, path: 'my.project') }
+ set(:project) { create(:project, path: 'my.project', pages_https_only: false) }
set(:user) { create(:user) }
set(:admin) { create(:admin) }
- set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) }
- set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) }
- set(:pages_domain_expired) { create(:pages_domain, :with_expired_certificate, :with_key, domain: 'expired.domain.test', project: project) }
+ set(:pages_domain) { create(:pages_domain, :without_key, :without_certificate, domain: 'www.domain.test', project: project) }
+ set(:pages_domain_secure) { create(:pages_domain, domain: 'ssl.domain.test', project: project) }
+ set(:pages_domain_expired) { create(:pages_domain, :with_expired_certificate, domain: 'expired.domain.test', project: project) }
- let(:pages_domain_params) { build(:pages_domain, domain: 'www.other-domain.test').slice(:domain) }
- let(:pages_domain_secure_params) { build(:pages_domain, :with_certificate, :with_key, domain: 'ssl.other-domain.test', project: project).slice(:domain, :certificate, :key) }
- let(:pages_domain_secure_key_missmatch_params) {build(:pages_domain, :with_trusted_chain, :with_key, project: project).slice(:domain, :certificate, :key) }
+ let(:pages_domain_params) { build(:pages_domain, :without_key, :without_certificate, domain: 'www.other-domain.test').slice(:domain) }
+ let(:pages_domain_secure_params) { build(:pages_domain, domain: 'ssl.other-domain.test', project: project).slice(:domain, :certificate, :key) }
+ let(:pages_domain_secure_key_missmatch_params) {build(:pages_domain, :with_trusted_chain, project: project).slice(:domain, :certificate, :key) }
let(:pages_domain_secure_missing_chain_params) {build(:pages_domain, :with_missing_chain, project: project).slice(:certificate) }
let(:route) { "/projects/#{project.id}/pages/domains" }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index cee93f6ed14..d73a42f48ad 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1718,6 +1718,12 @@ describe API::Projects do
group
end
+ let(:group3) do
+ group = create(:group, name: 'group3_name', parent: group2)
+ group.add_owner(user2)
+ group
+ end
+
before do
project.add_reporter(user2)
end
@@ -1813,6 +1819,15 @@ describe API::Projects do
expect(json_response['namespace']['name']).to eq(group2.name)
end
+ it 'forks to owned subgroup' do
+ full_path = "#{group2.path}/#{group3.path}"
+ post api("/projects/#{project.id}/fork", user2), namespace: full_path
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['namespace']['name']).to eq(group3.name)
+ expect(json_response['namespace']['full_path']).to eq(full_path)
+ end
+
it 'fails to fork to not owned group' do
post api("/projects/#{project.id}/fork", user2), namespace: group.name
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index 1d23e023bb6..576fde46615 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -193,6 +193,19 @@ describe API::ProtectedBranches do
expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
end
end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ post post_endpoint, name: branch_name
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
end
context 'when authenticated as a guest' do
@@ -209,18 +222,20 @@ describe API::ProtectedBranches do
end
describe "DELETE /projects/:id/protected_branches/unprotect/:branch" do
+ let(:delete_endpoint) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) }
+
before do
project.add_master(user)
end
it "unprotects a single branch" do
- delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user)
+ delete delete_endpoint
expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) }
+ let(:request) { delete_endpoint }
end
it "returns 404 if branch does not exist" do
@@ -229,11 +244,24 @@ describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(404)
end
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ delete delete_endpoint
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
context 'when branch has a wildcard in its name' do
let(:protected_name) { 'feature*' }
it "unprotects a wildcard branch" do
- delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user)
+ delete delete_endpoint
expect(response).to have_gitlab_http_status(204)
end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 95c23726a79..f3dd121faa9 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -200,7 +200,7 @@ describe API::Runner do
let(:project) { create(:project, shared_runners_enabled: false) }
let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
let(:runner) { create(:ci_runner) }
- let!(:job) do
+ let(:job) do
create(:ci_build, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
end
@@ -215,6 +215,7 @@ describe API::Runner do
let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' }
before do
+ job
stub_container_registry_config(enabled: false)
end
@@ -888,6 +889,7 @@ describe API::Runner do
let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
before do
+ stub_artifacts_object_storage
job.run!
end
@@ -1179,27 +1181,67 @@ describe API::Runner do
describe 'GET /api/v4/jobs/:id/artifacts' do
let(:token) { job.token }
- before do
- download_artifact
- end
-
context 'when job has artifacts' do
- let(:job) { create(:ci_build, :artifacts) }
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ let(:job) { create(:ci_build) }
+ let(:store) { JobArtifactUploader::Store::LOCAL }
+
+ before do
+ create(:ci_job_artifact, :archive, file_store: store, job: job)
end
context 'when using job token' do
- it 'download artifacts' do
- expect(response).to have_gitlab_http_status(200)
- expect(response.headers).to include download_headers
+ context 'when artifacts are stored locally' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ end
+
+ before do
+ download_artifact
+ end
+
+ it 'download artifacts' do
+ expect(response).to have_http_status(200)
+ expect(response.headers).to include download_headers
+ end
+ end
+
+ context 'when artifacts are stored remotely' do
+ let(:store) { JobArtifactUploader::Store::REMOTE }
+ let!(:job) { create(:ci_build) }
+
+ context 'when proxy download is being used' do
+ before do
+ download_artifact(direct_download: false)
+ end
+
+ it 'uses workhorse send-url' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers).to include(
+ 'Gitlab-Workhorse-Send-Data' => /send-url:/)
+ end
+ end
+
+ context 'when direct download is being used' do
+ before do
+ download_artifact(direct_download: true)
+ end
+
+ it 'receive redirect for downloading artifacts' do
+ expect(response).to have_gitlab_http_status(302)
+ expect(response.headers).to include('Location')
+ end
+ end
end
end
context 'when using runnners token' do
let(:token) { job.project.runners_token }
+ before do
+ download_artifact
+ end
+
it 'responds with forbidden' do
expect(response).to have_gitlab_http_status(403)
end
@@ -1208,12 +1250,16 @@ describe API::Runner do
context 'when job does not has artifacts' do
it 'responds with not found' do
+ download_artifact
+
expect(response).to have_gitlab_http_status(404)
end
end
def download_artifact(params = {}, request_headers = headers)
params = params.merge(token: token)
+ job.reload
+
get api("/jobs/#{job.id}/artifacts"), params, request_headers
end
end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 9052a18c60b..f8d5258a8d9 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -99,10 +99,10 @@ describe API::Search do
end
end
- describe "GET /groups/:id/-/search" do
+ describe "GET /groups/:id/search" do
context 'when user is not authenticated' do
it 'returns 401 error' do
- get api("/groups/#{group.id}/-/search"), scope: 'projects', search: 'awesome'
+ get api("/groups/#{group.id}/search"), scope: 'projects', search: 'awesome'
expect(response).to have_gitlab_http_status(401)
end
@@ -110,7 +110,7 @@ describe API::Search do
context 'when scope is not supported' do
it 'returns 400 error' do
- get api("/groups/#{group.id}/-/search", user), scope: 'unsupported', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'unsupported', search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -118,7 +118,7 @@ describe API::Search do
context 'when scope is missing' do
it 'returns 400 error' do
- get api("/groups/#{group.id}/-/search", user), search: 'awesome'
+ get api("/groups/#{group.id}/search", user), search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -126,7 +126,7 @@ describe API::Search do
context 'when group does not exist' do
it 'returns 404 error' do
- get api('/groups/9999/-/search', user), scope: 'issues', search: 'awesome'
+ get api('/groups/9999/search', user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -136,7 +136,7 @@ describe API::Search do
it 'returns 404 error' do
private_group = create(:group, :private)
- get api("/groups/#{private_group.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/groups/#{private_group.id}/search", user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -145,7 +145,7 @@ describe API::Search do
context 'with correct params' do
context 'for projects scope' do
before do
- get api("/groups/#{group.id}/-/search", user), scope: 'projects', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'projects', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/projects'
@@ -155,7 +155,7 @@ describe API::Search do
before do
create(:issue, project: project, title: 'awesome issue')
- get api("/groups/#{group.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'issues', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
@@ -165,7 +165,7 @@ describe API::Search do
before do
create(:merge_request, source_project: repo_project, title: 'awesome mr')
- get api("/groups/#{group.id}/-/search", user), scope: 'merge_requests', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'merge_requests', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
@@ -175,7 +175,7 @@ describe API::Search do
before do
create(:milestone, project: project, title: 'awesome milestone')
- get api("/groups/#{group.id}/-/search", user), scope: 'milestones', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'milestones', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
@@ -187,7 +187,7 @@ describe API::Search do
create(:milestone, project: project, title: 'awesome milestone')
create(:milestone, project: another_project, title: 'awesome milestone other project')
- get api("/groups/#{CGI.escape(group.full_path)}/-/search", user), scope: 'milestones', search: 'awesome'
+ get api("/groups/#{CGI.escape(group.full_path)}/search", user), scope: 'milestones', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
@@ -198,7 +198,7 @@ describe API::Search do
describe "GET /projects/:id/search" do
context 'when user is not authenticated' do
it 'returns 401 error' do
- get api("/projects/#{project.id}/-/search"), scope: 'issues', search: 'awesome'
+ get api("/projects/#{project.id}/search"), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(401)
end
@@ -206,7 +206,7 @@ describe API::Search do
context 'when scope is not supported' do
it 'returns 400 error' do
- get api("/projects/#{project.id}/-/search", user), scope: 'unsupported', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'unsupported', search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -214,7 +214,7 @@ describe API::Search do
context 'when scope is missing' do
it 'returns 400 error' do
- get api("/projects/#{project.id}/-/search", user), search: 'awesome'
+ get api("/projects/#{project.id}/search", user), search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -222,7 +222,7 @@ describe API::Search do
context 'when project does not exist' do
it 'returns 404 error' do
- get api('/projects/9999/-/search', user), scope: 'issues', search: 'awesome'
+ get api('/projects/9999/search', user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -232,7 +232,7 @@ describe API::Search do
it 'returns 404 error' do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- get api("/projects/#{project.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -243,7 +243,7 @@ describe API::Search do
before do
create(:issue, project: project, title: 'awesome issue')
- get api("/projects/#{project.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'issues', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
@@ -253,7 +253,7 @@ describe API::Search do
before do
create(:merge_request, source_project: repo_project, title: 'awesome mr')
- get api("/projects/#{repo_project.id}/-/search", user), scope: 'merge_requests', search: 'awesome'
+ get api("/projects/#{repo_project.id}/search", user), scope: 'merge_requests', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
@@ -263,7 +263,7 @@ describe API::Search do
before do
create(:milestone, project: project, title: 'awesome milestone')
- get api("/projects/#{project.id}/-/search", user), scope: 'milestones', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'milestones', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
@@ -273,7 +273,7 @@ describe API::Search do
before do
create(:note_on_merge_request, project: project, note: 'awesome note')
- get api("/projects/#{project.id}/-/search", user), scope: 'notes', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'notes', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/notes'
@@ -284,7 +284,7 @@ describe API::Search do
wiki = create(:project_wiki, project: project)
create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: "Awesome page" })
- get api("/projects/#{project.id}/-/search", user), scope: 'wiki_blobs', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'wiki_blobs', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs'
@@ -292,7 +292,7 @@ describe API::Search do
context 'for commits scope' do
before do
- get api("/projects/#{repo_project.id}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
+ get api("/projects/#{repo_project.id}/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details'
@@ -300,7 +300,7 @@ describe API::Search do
context 'for commits scope with project path as id' do
before do
- get api("/projects/#{CGI.escape(repo_project.full_path)}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
+ get api("/projects/#{CGI.escape(repo_project.full_path)}/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details'
@@ -308,7 +308,7 @@ describe API::Search do
context 'for blobs scope' do
before do
- get api("/projects/#{repo_project.id}/-/search", user), scope: 'blobs', search: 'monitors'
+ get api("/projects/#{repo_project.id}/search", user), scope: 'blobs', search: 'monitors'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs', size: 2
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index de1619f33c1..6bb53fdc98d 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -65,7 +65,7 @@ describe API::Templates do
expect(json_response['description']).to include('A short and simple permissive license with conditions')
expect(json_response['conditions']).to eq(%w[include-copyright])
expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
- expect(json_response['limitations']).to eq(%w[no-liability])
+ expect(json_response['limitations']).to eq(%w[liability warranty])
expect(json_response['content']).to include('MIT License')
end
end
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index 79041c6a792..00f067889a0 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -216,6 +216,7 @@ describe API::V3::Builds do
describe 'GET /projects/:id/builds/:build_id/artifacts' do
before do
+ stub_artifacts_object_storage
get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
end
@@ -230,13 +231,24 @@ describe API::V3::Builds do
end
it 'returns specific job artifacts' do
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to have_http_status(200)
expect(response.headers).to include(download_headers)
expect(response.body).to match_file(build.artifacts_file.file.file)
end
end
end
+ context 'when artifacts are stored remotely' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: build) }
+
+ it 'returns location redirect' do
+ get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
context 'unauthorized user' do
let(:api_user) { nil }
@@ -256,6 +268,7 @@ describe API::V3::Builds do
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
before do
+ stub_artifacts_object_storage
build.success
end
@@ -318,9 +331,24 @@ describe API::V3::Builds do
"attachment; filename=#{build.artifacts_file.filename}" }
end
- it { expect(response).to have_gitlab_http_status(200) }
+ it { expect(response).to have_http_status(200) }
it { expect(response.headers).to include(download_headers) }
end
+
+ context 'when artifacts are stored remotely' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: build) }
+
+ before do
+ build.reload
+
+ get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
+ end
+
+ it 'returns location redirect' do
+ expect(response).to have_http_status(302)
+ end
+ end
end
context 'with regular branch' do
diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb
index 38a8994eb79..1a637f3cf96 100644
--- a/spec/requests/api/v3/templates_spec.rb
+++ b/spec/requests/api/v3/templates_spec.rb
@@ -57,7 +57,7 @@ describe API::V3::Templates do
expect(json_response['description']).to include('A short and simple permissive license with conditions')
expect(json_response['conditions']).to eq(%w[include-copyright])
expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
- expect(json_response['limitations']).to eq(%w[no-liability])
+ expect(json_response['limitations']).to eq(%w[liability warranty])
expect(json_response['content']).to include('MIT License')
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 6dbbb1ad7bb..494db30e8e0 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -163,7 +163,7 @@ describe 'Git HTTP requests' do
download(path) do |response|
json_body = ActiveSupport::JSON.decode(response.body)
- expect(json_body['RepoPath']).to include(wiki.repository.disk_path)
+ expect(json_body['Repository']['relative_path']).to eq(wiki.repository.relative_path)
end
end
end
@@ -344,20 +344,11 @@ describe 'Git HTTP requests' do
context 'and the user requests a redirected path' do
let!(:redirect) { project.route.create_redirect('foo/bar') }
let(:path) { "#{redirect.path}.git" }
- let(:project_moved_message) do
- <<-MSG.strip_heredoc
- Project '#{redirect.path}' was moved to '#{project.full_path}'.
- Please update your Git remote:
-
- git remote set-url origin #{project.http_url_to_repo} and try again.
- MSG
- end
-
- it 'downloads get status 404 with "project was moved" message' do
+ it 'downloads get status 200 for redirects' do
clone_get(path, {})
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to match(project_moved_message)
+
+ expect(response).to have_gitlab_http_status(:ok)
end
end
end
@@ -559,20 +550,19 @@ describe 'Git HTTP requests' do
Please update your Git remote:
- git remote set-url origin #{project.http_url_to_repo} and try again.
+ git remote set-url origin #{project.http_url_to_repo}.
MSG
end
- it 'downloads get status 404 with "project was moved" message' do
+ it 'downloads get status 200' do
clone_get(path, env)
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to match(project_moved_message)
+
+ expect(response).to have_gitlab_http_status(:ok)
end
it 'uploads get status 404 with "project was moved" message' do
upload(path, env) do |response|
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to match(project_moved_message)
+ expect(response).to have_gitlab_http_status(:ok)
end
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 971b45c411d..1e6bd993c08 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -191,10 +191,12 @@ describe 'Git LFS API and storage' do
describe 'when fetching lfs object' do
let(:project) { create(:project) }
let(:update_permissions) { }
+ let(:before_get) { }
before do
enable_lfs
update_permissions
+ before_get
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
end
@@ -239,6 +241,38 @@ describe 'Git LFS API and storage' do
end
it_behaves_like 'responds with a file'
+
+ context 'when LFS uses object storage' do
+ context 'when proxy download is enabled' do
+ let(:before_get) do
+ stub_lfs_object_storage(proxy_download: true)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
+
+ it 'responds with redirect' do
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'responds with the workhorse send-url' do
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
+ end
+ end
+
+ context 'when proxy download is disabled' do
+ let(:before_get) do
+ stub_lfs_object_storage(proxy_download: false)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
+
+ it 'responds with redirect' do
+ expect(response).to have_gitlab_http_status(302)
+ end
+
+ it 'responds with the file location' do
+ expect(response.location).to include(lfs_object.reload.file.path)
+ end
+ end
+ end
end
end
@@ -945,22 +979,61 @@ describe 'Git LFS API and storage' do
end
context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- put_authorize
+ shared_examples 'a valid response' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'uses the gitlab-workhorse content type' do
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
+ shared_examples 'a local file' do
+ it_behaves_like 'a valid response' do
+ it 'responds with status 200, location of lfs store and object details' do
+ expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
end
- it 'uses the gitlab-workhorse content type' do
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ context 'when using local storage' do
+ it_behaves_like 'a local file'
end
- it 'responds with status 200, location of lfs store and object details' do
- expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path)
- expect(json_response['LfsOid']).to eq(sample_oid)
- expect(json_response['LfsSize']).to eq(sample_size)
+ context 'when using remote storage' do
+ context 'when direct upload is enabled' do
+ before do
+ stub_lfs_object_storage(enabled: true, direct_upload: true)
+ end
+
+ it_behaves_like 'a valid response' do
+ it 'responds with status 200, location of lfs remote store and object details' do
+ expect(json_response['TempPath']).to be_nil
+ expect(json_response['RemoteObject']).to have_key('ID')
+ expect(json_response['RemoteObject']).to have_key('GetURL')
+ expect(json_response['RemoteObject']).to have_key('StoreURL')
+ expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_lfs_object_storage(enabled: true, direct_upload: false)
+ end
+
+ it_behaves_like 'a local file'
+ end
end
end
@@ -978,14 +1051,95 @@ describe 'Git LFS API and storage' do
end
end
+ context 'and workhorse requests upload finalize for a new lfs object' do
+ before do
+ lfs_object.destroy
+ end
+
+ context 'with object storage disabled' do
+ it "doesn't attempt to migrate file to object storage" do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ put_finalize(with_tempfile: true)
+ end
+ end
+
+ context 'with object storage enabled' do
+ context 'and direct upload enabled' do
+ let!(:fog_connection) do
+ stub_lfs_object_storage(direct_upload: true)
+ end
+
+ ['123123', '../../123123'].each do |remote_id|
+ context "with invalid remote_id: #{remote_id}" do
+ subject do
+ put_finalize_with_args('file.remote_id' => remote_id)
+ end
+
+ it 'responds with status 403' do
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'with valid remote_id' do
+ before do
+ fog_connection.directories.get('lfs-objects').files.create(
+ key: 'tmp/upload/12312300',
+ body: 'content'
+ )
+ end
+
+ subject do
+ put_finalize_with_args(
+ 'file.remote_id' => '12312300',
+ 'file.name' => 'name')
+ end
+
+ it 'responds with status 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'schedules migration of file to object storage' do
+ subject
+
+ expect(LfsObject.last.projects).to include(project)
+ end
+
+ it 'have valid file' do
+ subject
+
+ expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(LfsObject.last.file).to be_exists
+ end
+ end
+ end
+
+ context 'and background upload enabled' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
+
+ it 'schedules migration of file to object storage' do
+ expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric))
+
+ put_finalize(with_tempfile: true)
+ end
+ end
+ end
+ end
+
context 'invalid tempfiles' do
- it 'rejects slashes in the tempfile name (path traversal' do
- put_finalize('foo/bar')
- expect(response).to have_gitlab_http_status(403)
+ before do
+ lfs_object.destroy
end
- it 'rejects tempfile names that do not start with the oid' do
- put_finalize("foo#{sample_oid}")
+ it 'rejects slashes in the tempfile name (path traversal)' do
+ put_finalize('../bar', with_tempfile: true)
expect(response).to have_gitlab_http_status(403)
end
end
@@ -1075,7 +1229,7 @@ describe 'Git LFS API and storage' do
end
it 'with location of lfs store and object details' do
- expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path)
+ expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end
@@ -1177,9 +1331,25 @@ describe 'Git LFS API and storage' do
put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, authorize_headers
end
- def put_finalize(lfs_tmp = lfs_tmp_file)
- put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", nil,
- headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp).compact
+ def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false)
+ upload_path = LfsObjectUploader.workhorse_local_upload_path
+ file_path = upload_path + '/' + lfs_tmp if lfs_tmp
+
+ if with_tempfile
+ FileUtils.mkdir_p(upload_path)
+ FileUtils.touch(file_path)
+ end
+
+ args = {
+ 'file.path' => file_path,
+ 'file.name' => File.basename(file_path)
+ }.compact
+
+ put_finalize_with_args(args)
+ end
+
+ def put_finalize_with_args(args)
+ put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", args, headers
end
def lfs_tmp_file
diff --git a/spec/rubocop/cop/gitlab/httparty_spec.rb b/spec/rubocop/cop/gitlab/httparty_spec.rb
new file mode 100644
index 00000000000..510839a21d7
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/httparty_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/httparty'
+
+describe RuboCop::Cop::Gitlab::HTTParty do # rubocop:disable RSpec/FilePath
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ shared_examples('registering include offense') do |options|
+ let(:offending_lines) { options[:offending_lines] }
+
+ it 'registers an offense when the class includes HTTParty' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(offending_lines.size)
+ expect(cop.offenses.map(&:line)).to eq(offending_lines)
+ end
+ end
+ end
+
+ shared_examples('registering call offense') do |options|
+ let(:offending_lines) { options[:offending_lines] }
+
+ it 'registers an offense when the class calls HTTParty' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(offending_lines.size)
+ expect(cop.offenses.map(&:line)).to eq(offending_lines)
+ end
+ end
+ end
+
+ context 'when source is a regular module' do
+ it_behaves_like 'registering include offense', offending_lines: [2] do
+ let(:source) do
+ <<~RUBY
+ module M
+ include HTTParty
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is a regular class' do
+ it_behaves_like 'registering include offense', offending_lines: [2] do
+ let(:source) do
+ <<~RUBY
+ class Foo
+ include HTTParty
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when HTTParty is called' do
+ it_behaves_like 'registering call offense', offending_lines: [3] do
+ let(:source) do
+ <<~RUBY
+ class Foo
+ def bar
+ HTTParty.get('http://example.com')
+ end
+ end
+ RUBY
+ end
+ end
+ end
+end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index c38795ad1a1..f51c11b141f 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -117,6 +117,7 @@ describe PipelineSerializer do
shared_examples 'no N+1 queries' do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
+
expect(recorded.count).to be_within(1).of(36)
expect(recorded.cached_count).to eq(0)
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 0ce41e7c7ee..feb5120bc68 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -9,6 +9,8 @@ describe Ci::ProcessPipelineService, '#execute' do
end
before do
+ stub_ci_pipeline_to_return_yaml_file
+
stub_not_protect_default_branch
project.add_developer(user)
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index db9c216d3f4..b86a3d72bb4 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -28,7 +28,8 @@ describe Ci::RetryBuildService do
%i[type lock_version target_url base_tags trace_sections
commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id
- user_id auto_canceled_by_id retried failure_reason].freeze
+ user_id auto_canceled_by_id retried failure_reason
+ artifacts_file_store artifacts_metadata_store].freeze
shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index e2e64659dfa..1c2f9c5cf43 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -82,7 +82,7 @@ describe Clusters::CreateService do
context 'when project has a cluster' do
include_context 'valid params'
- let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
it 'does not create a cluster' do
expect(ClusterProvisionWorker).not_to receive(:perform_async)
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index e1c873f8c1e..999677cfaaa 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -222,8 +222,8 @@ describe Groups::TransferService, :postgresql do
expect(new_parent_group.children.first).to eq(group)
end
- it 'should create a permanent redirect for the group' do
- expect(group.redirect_routes.permanent.count).to eq(1)
+ it 'should create a redirect for the group' do
+ expect(group.redirect_routes.count).to eq(1)
end
end
@@ -243,10 +243,10 @@ describe Groups::TransferService, :postgresql do
end
end
- it 'should create permanent redirects for the subgroups' do
- expect(group.redirect_routes.permanent.count).to eq(1)
- expect(subgroup1.redirect_routes.permanent.count).to eq(1)
- expect(subgroup2.redirect_routes.permanent.count).to eq(1)
+ it 'should create redirects for the subgroups' do
+ expect(group.redirect_routes.count).to eq(1)
+ expect(subgroup1.redirect_routes.count).to eq(1)
+ expect(subgroup2.redirect_routes.count).to eq(1)
end
context 'when the new parent has a higher visibility than the children' do
@@ -287,9 +287,9 @@ describe Groups::TransferService, :postgresql do
end
it 'should create permanent redirects for the projects' do
- expect(group.redirect_routes.permanent.count).to eq(1)
- expect(project1.redirect_routes.permanent.count).to eq(1)
- expect(project2.redirect_routes.permanent.count).to eq(1)
+ expect(group.redirect_routes.count).to eq(1)
+ expect(project1.redirect_routes.count).to eq(1)
+ expect(project2.redirect_routes.count).to eq(1)
end
context 'when the new parent has a higher visibility than the projects' do
@@ -338,12 +338,12 @@ describe Groups::TransferService, :postgresql do
end
end
- it 'should create permanent redirect for the subgroups and projects' do
- expect(group.redirect_routes.permanent.count).to eq(1)
- expect(subgroup1.redirect_routes.permanent.count).to eq(1)
- expect(subgroup2.redirect_routes.permanent.count).to eq(1)
- expect(project1.redirect_routes.permanent.count).to eq(1)
- expect(project2.redirect_routes.permanent.count).to eq(1)
+ it 'should create redirect for the subgroups and projects' do
+ expect(group.redirect_routes.count).to eq(1)
+ expect(subgroup1.redirect_routes.count).to eq(1)
+ expect(subgroup2.redirect_routes.count).to eq(1)
+ expect(project1.redirect_routes.count).to eq(1)
+ expect(project2.redirect_routes.count).to eq(1)
end
end
@@ -380,12 +380,12 @@ describe Groups::TransferService, :postgresql do
end
end
- it 'should create permanent redirect for the subgroups and projects' do
- expect(group.redirect_routes.permanent.count).to eq(1)
- expect(project1.redirect_routes.permanent.count).to eq(1)
- expect(subgroup1.redirect_routes.permanent.count).to eq(1)
- expect(nested_subgroup.redirect_routes.permanent.count).to eq(1)
- expect(nested_project.redirect_routes.permanent.count).to eq(1)
+ it 'should create redirect for the subgroups and projects' do
+ expect(group.redirect_routes.count).to eq(1)
+ expect(project1.redirect_routes.count).to eq(1)
+ expect(subgroup1.redirect_routes.count).to eq(1)
+ expect(nested_subgroup.redirect_routes.count).to eq(1)
+ expect(nested_project.redirect_routes.count).to eq(1)
end
end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index c148a98569b..a9aee9e100f 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -6,7 +6,7 @@ describe Issues::MoveService do
let(:title) { 'Some issue' }
let(:description) { 'Some issue description' }
let(:old_project) { create(:project) }
- let(:new_project) { create(:project, group: create(:group)) }
+ let(:new_project) { create(:project) }
let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') }
let(:old_issue) do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 903aa0a5078..2536c6e2514 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -24,6 +24,14 @@ describe MergeRequests::RefreshService do
merge_when_pipeline_succeeds: true,
merge_user: @user)
+ @another_merge_request = create(:merge_request,
+ source_project: @project,
+ source_branch: 'master',
+ target_branch: 'test',
+ target_project: @project,
+ merge_when_pipeline_succeeds: true,
+ merge_user: @user)
+
@fork_merge_request = create(:merge_request,
source_project: @fork_project,
source_branch: 'master',
@@ -52,9 +60,11 @@ describe MergeRequests::RefreshService do
context 'push to origin repo source branch' do
let(:refresh_service) { service.new(@project, @user) }
+ let(:notification_service) { spy('notification_service') }
before do
allow(refresh_service).to receive(:execute_hooks)
+ allow(NotificationService).to receive(:new) { notification_service }
end
it 'executes hooks with update action' do
@@ -64,6 +74,11 @@ describe MergeRequests::RefreshService do
expect(refresh_service).to have_received(:execute_hooks)
.with(@merge_request, 'update', old_rev: @oldrev)
+ expect(notification_service).to have_received(:push_to_merge_request)
+ .with(@merge_request, @user, new_commits: anything, existing_commits: anything)
+ expect(notification_service).to have_received(:push_to_merge_request)
+ .with(@another_merge_request, @user, new_commits: anything, existing_commits: anything)
+
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
@@ -119,11 +134,13 @@ describe MergeRequests::RefreshService do
context 'push to origin repo source branch when an MR was reopened' do
let(:refresh_service) { service.new(@project, @user) }
+ let(:notification_service) { spy('notification_service') }
before do
@merge_request.update(state: :reopened)
allow(refresh_service).to receive(:execute_hooks)
+ allow(NotificationService).to receive(:new) { notification_service }
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
end
@@ -131,6 +148,10 @@ describe MergeRequests::RefreshService do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks)
.with(@merge_request, 'update', old_rev: @oldrev)
+ expect(notification_service).to have_received(:push_to_merge_request)
+ .with(@merge_request, @user, new_commits: anything, existing_commits: anything)
+ expect(notification_service).to have_received(:push_to_merge_request)
+ .with(@another_merge_request, @user, new_commits: anything, existing_commits: anything)
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 62fdf870090..f8fa2540804 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -34,6 +34,12 @@ describe NotificationService, :mailer do
should_not_email_anyone
end
+ it 'emails new mentions despite being unsubscribed' do
+ send_notifications(@unsubscribed_mentioned)
+
+ should_only_email(@unsubscribed_mentioned)
+ end
+
it 'sends the proper notification reason header' do
send_notifications(@u_watcher)
should_only_email(@u_watcher)
@@ -122,7 +128,7 @@ describe NotificationService, :mailer do
let(:project) { create(:project, :private) }
let(:issue) { create(:issue, project: project, assignees: [assignee]) }
let(:mentioned_issue) { create(:issue, assignees: issue.assignees) }
- let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @outsider also') }
+ let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @unsubscribed_mentioned and @outsider also') }
before do
build_team(note.project)
@@ -150,7 +156,7 @@ describe NotificationService, :mailer do
add_users_with_subscription(note.project, issue)
reset_delivered_emails!
- expect(SentNotification).to receive(:record).with(issue, any_args).exactly(9).times
+ expect(SentNotification).to receive(:record).with(issue, any_args).exactly(10).times
notification.new_note(note)
@@ -163,6 +169,7 @@ describe NotificationService, :mailer do
should_email(@watcher_and_subscriber)
should_email(@subscribed_participant)
should_email(@u_custom_off)
+ should_email(@unsubscribed_mentioned)
should_not_email(@u_guest_custom)
should_not_email(@u_guest_watcher)
should_not_email(note.author)
@@ -279,6 +286,7 @@ describe NotificationService, :mailer do
before do
build_team(note.project)
note.project.add_master(note.author)
+ add_users_with_subscription(note.project, issue)
reset_delivered_emails!
end
@@ -286,6 +294,9 @@ describe NotificationService, :mailer do
it 'notifies the team members' do
notification.new_note(note)
+ # Make sure @unsubscribed_mentioned is part of the team
+ expect(note.project.team.members).to include(@unsubscribed_mentioned)
+
# Notify all team members
note.project.team.members.each do |member|
# User with disabled notification should not be notified
@@ -486,7 +497,7 @@ describe NotificationService, :mailer do
let(:group) { create(:group) }
let(:project) { create(:project, :public, namespace: group) }
let(:another_project) { create(:project, :public, namespace: group) }
- let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant' }
+ let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant @unsubscribed_mentioned' }
before do
build_team(issue.project)
@@ -510,6 +521,7 @@ describe NotificationService, :mailer do
should_email(@u_participant_mentioned)
should_email(@g_global_watcher)
should_email(@g_watcher)
+ should_email(@unsubscribed_mentioned)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -1078,6 +1090,36 @@ describe NotificationService, :mailer do
end
end
+ describe '#push_to_merge_request' do
+ before do
+ update_custom_notification(:push_to_merge_request, @u_guest_custom, resource: project)
+ update_custom_notification(:push_to_merge_request, @u_custom_global)
+ end
+
+ it do
+ notification.push_to_merge_request(merge_request, @u_disabled)
+
+ should_email(merge_request.assignee)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@u_watcher)
+ should_not_email(@u_guest_watcher)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { merge_request }
+ let(:notification_trigger) { notification.push_to_merge_request(merge_request, @u_disabled) }
+ end
+ end
+
describe '#relabel_merge_request' do
let(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1', merge_requests: [merge_request]) }
let(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
@@ -1823,6 +1865,7 @@ describe NotificationService, :mailer do
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
+ @unsubscribed_mentioned = create :user, username: 'unsubscribed_mentioned'
@subscribed_participant = create_global_setting_for(create(:user, username: 'subscribed_participant'), :participating)
@watcher_and_subscriber = create_global_setting_for(create(:user), :watch)
@@ -1830,7 +1873,9 @@ describe NotificationService, :mailer do
project.add_master(@subscriber)
project.add_master(@unsubscriber)
project.add_master(@watcher_and_subscriber)
+ project.add_master(@unsubscribed_mentioned)
+ issuable.subscriptions.create(user: @unsubscribed_mentioned, project: project, subscribed: false)
issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true)
issuable.subscriptions.create(user: @subscribed_participant, project: project, subscribed: true)
issuable.subscriptions.create(user: @unsubscriber, project: project, subscribed: false)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 8471467d2fa..4413c6ef83e 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -153,7 +153,7 @@ describe Projects::CreateService, '#execute' do
context 'when another repository already exists on disk' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
let(:opts) do
{
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index d1011b07db6..0f7c46367d0 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -105,7 +105,7 @@ describe Projects::ForkService do
context 'repository already exists' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
before do
gitlab_shell.create_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index ce567fe3879..95a6771c59d 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -146,7 +146,7 @@ describe Projects::TransferService do
context 'namespace which contains orphan repository with same projects path name' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
before do
group.add_owner(user)
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index f3f97b6b921..f48d466d263 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -190,7 +190,7 @@ describe Projects::UpdateService do
context 'when renaming a project' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
context 'with legacy storage' do
let(:project) { create(:project, :legacy_storage, :repository, creator: user, namespace: user.namespace) }
@@ -241,6 +241,27 @@ describe Projects::UpdateService do
})
end
end
+
+ context 'when updating #pages_https_only', :https_pages_enabled do
+ subject(:call_service) do
+ update_project(project, admin, pages_https_only: false)
+ end
+
+ it 'updates the attribute' do
+ expect { call_service }
+ .to change { project.pages_https_only? }
+ .to(false)
+ end
+
+ it 'calls Projects::UpdatePagesConfigurationService' do
+ expect(Projects::UpdatePagesConfigurationService)
+ .to receive(:new)
+ .with(project)
+ .and_call_original
+
+ call_service
+ end
+ end
end
describe '#run_auto_devops_pipeline?' do
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 53b3e5e365d..786493c3577 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -35,5 +35,18 @@ describe ProtectedBranches::CreateService do
expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
+
+ context 'when a policy restricts rule creation' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents creation of the protected branch rule" do
+ expect do
+ service.execute
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
end
end
diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb
new file mode 100644
index 00000000000..4a391b6c25c
--- /dev/null
+++ b/spec/services/protected_branches/destroy_service_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe ProtectedBranches::DestroyService do
+ let(:protected_branch) { create(:protected_branch) }
+ let(:project) { protected_branch.project }
+ let(:user) { project.owner }
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user) }
+
+ it 'destroys a protected branch' do
+ service.execute(protected_branch)
+
+ expect(protected_branch).to be_destroyed
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ expect do
+ service.execute(protected_branch)
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+ end
+end
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index 9fa5983db66..3f6f8e09565 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -22,5 +22,16 @@ describe ProtectedBranches::UpdateService do
expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
+
+ context 'when a policy restricts rule creation' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents creation of the protected branch rule" do
+ expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
end
end
diff --git a/spec/services/protected_tags/destroy_service_spec.rb b/spec/services/protected_tags/destroy_service_spec.rb
new file mode 100644
index 00000000000..e12f53a2221
--- /dev/null
+++ b/spec/services/protected_tags/destroy_service_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe ProtectedTags::DestroyService do
+ let(:protected_tag) { create(:protected_tag) }
+ let(:project) { protected_tag.project }
+ let(:user) { project.owner }
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user) }
+
+ it 'destroy a protected tag' do
+ service.execute(protected_tag)
+
+ expect(protected_tag).to be_destroyed
+ end
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index a3893188c6e..e28b0ea5cf2 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -743,7 +743,7 @@ describe SystemNoteService do
expect(cross_reference(type)).to eq("Events for #{type.pluralize.humanize.downcase} are disabled.")
end
- it "blocks cross reference when #{type.underscore}_events is true" do
+ it "creates cross reference when #{type.underscore}_events is true" do
jira_tracker.update("#{type}_events" => true)
expect(cross_reference(type)).to eq(success_message)
diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb
index 576db1dde2d..d974cc0226f 100644
--- a/spec/services/verify_pages_domain_service_spec.rb
+++ b/spec/services/verify_pages_domain_service_spec.rb
@@ -93,6 +93,25 @@ describe VerifyPagesDomainService do
expect(domain).not_to be_enabled
end
end
+
+ context 'invalid domain' do
+ let(:domain) { build(:pages_domain, :expired, :with_missing_chain) }
+
+ before do
+ domain.save(validate: false)
+ end
+
+ it 'can be disabled' do
+ error_status[:message] += '. It is now disabled.'
+
+ stub_resolver
+
+ expect(service.execute).to eq(error_status)
+
+ expect(domain).not_to be_verified
+ expect(domain).not_to be_enabled
+ end
+ end
end
context 'timeout behaviour' do
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 21910e69d2e..2ef2e61babc 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -14,6 +14,20 @@ describe WebHookService do
end
let(:service_instance) { described_class.new(project_hook, data, :push_hooks) }
+ describe '#initialize' do
+ it 'allow_local_requests is true if hook is a SystemHook' do
+ instance = described_class.new(build(:system_hook), data, :system_hook)
+ expect(instance.request_options[:allow_local_requests]).to be_truthy
+ end
+
+ it 'allow_local_requests is false if hook is not a SystemHook' do
+ %i(project_hook service_hook web_hook_log).each do |hook|
+ instance = described_class.new(build(hook), data, hook)
+ expect(instance.request_options[:allow_local_requests]).to be_falsey
+ end
+ end
+ end
+
describe '#execute' do
before do
project.hooks << [project_hook]
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 9f6f0204a16..e8cecf361ff 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -97,6 +97,10 @@ RSpec.configure do |config|
TestEnv.init
end
+ config.after(:all) do
+ TestEnv.clean_test_path
+ end
+
config.before(:example) do
# Skip pre-receive hook check so we can use the web editor and merge.
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
@@ -197,6 +201,22 @@ RSpec.configure do |config|
Ability.allowed?(*args)
end
end
+
+ config.before(:each, :http_pages_enabled) do |_|
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
+ end
+
+ config.before(:each, :https_pages_enabled) do |_|
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443'])
+ end
+
+ config.before(:each, :http_pages_disabled) do |_|
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(false)
+ end
+
+ config.before(:each, :https_pages_disabled) do |_|
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(false)
+ end
end
# add simpler way to match asset paths containing digest strings
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
index c7e8a39a617..9cf541372b5 100644
--- a/spec/support/gitaly.rb
+++ b/spec/support/gitaly.rb
@@ -1,11 +1,13 @@
RSpec.configure do |config|
config.before(:each) do |example|
if example.metadata[:disable_gitaly]
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
+ # Use 'and_wrap_original' to make sure the arguments are valid
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) && false }
else
next if example.metadata[:skip_gitaly_mock]
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+ # Use 'and_wrap_original' to make sure the arguments are valid
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) || true }
end
end
end
diff --git a/spec/support/http_io/http_io_helpers.rb b/spec/support/http_io/http_io_helpers.rb
new file mode 100644
index 00000000000..31e07e720cd
--- /dev/null
+++ b/spec/support/http_io/http_io_helpers.rb
@@ -0,0 +1,64 @@
+module HttpIOHelpers
+ def stub_remote_trace_206
+ WebMock.stub_request(:get, remote_trace_url)
+ .to_return { |request| remote_trace_response(request, 206) }
+ end
+
+ def stub_remote_trace_200
+ WebMock.stub_request(:get, remote_trace_url)
+ .to_return { |request| remote_trace_response(request, 200) }
+ end
+
+ def stub_remote_trace_500
+ WebMock.stub_request(:get, remote_trace_url)
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def remote_trace_url
+ "http://trace.com/trace"
+ end
+
+ def remote_trace_response(request, responce_status)
+ range = request.headers['Range'].match(/bytes=(\d+)-(\d+)/)
+
+ {
+ status: responce_status,
+ headers: remote_trace_response_headers(responce_status, range[1].to_i, range[2].to_i),
+ body: range_trace_body(range[1].to_i, range[2].to_i)
+ }
+ end
+
+ def remote_trace_response_headers(responce_status, from, to)
+ headers = { 'Content-Type' => 'text/plain' }
+
+ if responce_status == 206
+ headers.merge('Content-Range' => "bytes #{from}-#{to}/#{remote_trace_size}")
+ end
+
+ headers
+ end
+
+ def range_trace_body(from, to)
+ remote_trace_body[from..to]
+ end
+
+ def remote_trace_body
+ @remote_trace_body ||= File.read(expand_fixture_path('trace/sample_trace'))
+ end
+
+ def remote_trace_size
+ remote_trace_body.length
+ end
+
+ def set_smaller_buffer_size_than(file_size)
+ blocks = (file_size / 128)
+ new_size = (blocks / 2) * 128
+ stub_const("Gitlab::Ci::Trace::HttpIO::BUFFER_SIZE", new_size)
+ end
+
+ def set_larger_buffer_size_than(file_size)
+ blocks = (file_size / 128)
+ new_size = (blocks * 2) * 128
+ stub_const("Gitlab::Ci::Trace::HttpIO::BUFFER_SIZE", new_size)
+ end
+end
diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb
index d7acf8c0032..b615a8f54cf 100644
--- a/spec/support/shared_examples/controllers/variables_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb
@@ -16,19 +16,19 @@ shared_examples 'PATCH #update updates variables' do
let(:variable_attributes) do
{ id: variable.id,
key: variable.key,
- value: variable.value,
+ secret_value: variable.value,
protected: variable.protected?.to_s }
end
let(:new_variable_attributes) do
{ key: 'new_key',
- value: 'dummy_value',
+ secret_value: 'dummy_value',
protected: 'false' }
end
context 'with invalid new variable parameters' do
let(:variables_attributes) do
[
- variable_attributes.merge(value: 'other_value'),
+ variable_attributes.merge(secret_value: 'other_value'),
new_variable_attributes.merge(key: '...?')
]
end
@@ -52,7 +52,7 @@ shared_examples 'PATCH #update updates variables' do
let(:variables_attributes) do
[
new_variable_attributes,
- new_variable_attributes.merge(value: 'other_value')
+ new_variable_attributes.merge(secret_value: 'other_value')
]
end
@@ -74,7 +74,7 @@ shared_examples 'PATCH #update updates variables' do
context 'with valid new variable parameters' do
let(:variables_attributes) do
[
- variable_attributes.merge(value: 'other_value'),
+ variable_attributes.merge(secret_value: 'other_value'),
new_variable_attributes
]
end
diff --git a/spec/support/shared_examples/models/atomic_internal_id_spec.rb b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
new file mode 100644
index 00000000000..144af4fc475
--- /dev/null
+++ b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+shared_examples_for 'AtomicInternalId' do
+ describe '.has_internal_id' do
+ describe 'Module inclusion' do
+ subject { described_class }
+
+ it { is_expected.to include_module(AtomicInternalId) }
+ end
+
+ describe 'Validation' do
+ subject { instance }
+
+ before do
+ allow(InternalId).to receive(:generate_next).and_return(nil)
+ end
+
+ it { is_expected.to validate_presence_of(internal_id_attribute) }
+ it { is_expected.to validate_numericality_of(internal_id_attribute) }
+ end
+
+ describe 'internal id generation' do
+ subject { instance.save! }
+
+ it 'calls InternalId.generate_next and sets internal id attribute' do
+ iid = rand(1..1000)
+
+ expect(InternalId).to receive(:generate_next).with(instance, scope_attrs, usage, any_args).and_return(iid)
+ subject
+ expect(instance.public_send(internal_id_attribute)).to eq(iid)
+ end
+
+ it 'does not overwrite an existing internal id' do
+ instance.public_send("#{internal_id_attribute}=", 4711)
+
+ expect { subject }.not_to change { instance.public_send(internal_id_attribute) }
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
new file mode 100644
index 00000000000..cd9974cd6e2
--- /dev/null
+++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
@@ -0,0 +1,126 @@
+shared_context 'with storage' do |store, **stub_params|
+ before do
+ subject.object_store = store
+ end
+end
+
+shared_examples "migrates" do |to_store:, from_store: nil|
+ let(:to) { to_store }
+ let(:from) { from_store || subject.object_store }
+
+ def migrate(to)
+ subject.migrate!(to)
+ end
+
+ def checksum
+ Digest::SHA256.hexdigest(subject.read)
+ end
+
+ before do
+ migrate(from)
+ end
+
+ it 'returns corresponding file type' do
+ expect(subject).to be_an(CarrierWave::Uploader::Base)
+ expect(subject).to be_a(ObjectStorage::Concern)
+
+ if from == described_class::Store::REMOTE
+ expect(subject.file).to be_a(CarrierWave::Storage::Fog::File)
+ elsif from == described_class::Store::LOCAL
+ expect(subject.file).to be_a(CarrierWave::SanitizedFile)
+ else
+ raise 'Unexpected file type'
+ end
+ end
+
+ it 'does nothing when migrating to the current store' do
+ expect { migrate(from) }.not_to change { subject.object_store }.from(from)
+ end
+
+ it 'migrate to the specified store' do
+ from_checksum = checksum
+
+ expect { migrate(to) }.to change { subject.object_store }.from(from).to(to)
+ expect(checksum).to eq(from_checksum)
+ end
+
+ it 'removes the original file after the migration' do
+ original_file = subject.file.path
+ migrate(to)
+
+ expect(File.exist?(original_file)).to be_falsey
+ end
+
+ it 'can access to the original file during migration' do
+ file = subject.file
+
+ allow(subject).to receive(:delete_migrated_file) { } # Remove as a callback of :migrate
+ allow(subject).to receive(:record_upload) { } # Remove as a callback of :store (:record_upload)
+
+ expect(file.exists?).to be_truthy
+ expect { migrate(to) }.not_to change { file.exists? }
+ end
+
+ context 'when migrate! is not oqqupied by another process' do
+ it 'executes migrate!' do
+ expect(subject).to receive(:object_store=).at_least(1)
+
+ migrate(to)
+ end
+ end
+
+ context 'when migrate! is occupied by another process' do
+ let(:exclusive_lease_key) { "object_storage_migrate:#{subject.model.class}:#{subject.model.id}" }
+
+ before do
+ @uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
+ end
+
+ it 'does not execute migrate!' do
+ expect(subject).not_to receive(:unsafe_migrate!)
+
+ expect { migrate(to) }.to raise_error('Already running')
+ end
+
+ after do
+ Gitlab::ExclusiveLease.cancel(exclusive_lease_key, @uuid)
+ end
+ end
+
+ context 'migration is unsuccessful' do
+ shared_examples "handles gracefully" do |error:|
+ it 'does not update the object_store' do
+ expect { migrate(to) }.to raise_error(error)
+ expect(subject.object_store).to eq(from)
+ end
+
+ it 'does not delete the original file' do
+ expect { migrate(to) }.to raise_error(error)
+ expect(subject.exists?).to be_truthy
+ end
+ end
+
+ context 'when the store is not supported' do
+ let(:to) { -1 } # not a valid store
+
+ include_examples "handles gracefully", error: ObjectStorage::UnknownStoreError
+ end
+
+ context 'upon a fog failure' do
+ before do
+ storage_class = subject.send(:storage_for, to).class
+ expect_any_instance_of(storage_class).to receive(:store!).and_raise("Store failure.")
+ end
+
+ include_examples "handles gracefully", error: "Store failure."
+ end
+
+ context 'upon a database failure' do
+ before do
+ expect(uploader).to receive(:persist_object_store!).and_raise("ActiveRecord failure.")
+ end
+
+ include_examples "handles gracefully", error: "ActiveRecord failure."
+ end
+ end
+end
diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb
index 52e47ae2d34..21995c89a6e 100644
--- a/spec/support/stored_repositories.rb
+++ b/spec/support/stored_repositories.rb
@@ -4,7 +4,7 @@ RSpec.configure do |config|
end
config.before(:all, :broken_storage) do
- FileUtils.rm_rf Gitlab.config.repositories.storages.broken['path']
+ FileUtils.rm_rf Gitlab.config.repositories.storages.broken.legacy_disk_path
end
config.before(:each, :broken_storage) do
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 9f08c139322..bad1d34df3a 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -50,8 +50,12 @@ module StubConfiguration
# Default storage is always required
messages['default'] ||= Gitlab.config.repositories.storages.default
- messages.each do |storage_name, storage_settings|
- storage_settings['path'] = TestEnv.repos_path unless storage_settings.key?('path')
+ messages.each do |storage_name, storage_hash|
+ if !storage_hash.key?('path') || storage_hash['path'] == Gitlab::GitalyClient::StorageSettings::Deprecated
+ storage_hash['path'] = TestEnv.repos_path
+ end
+
+ messages[storage_name] = Gitlab::GitalyClient::StorageSettings.new(storage_hash.to_h)
end
allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages))
diff --git a/spec/support/stub_object_storage.rb b/spec/support/stub_object_storage.rb
new file mode 100644
index 00000000000..6e88641da42
--- /dev/null
+++ b/spec/support/stub_object_storage.rb
@@ -0,0 +1,48 @@
+module StubConfiguration
+ def stub_object_storage_uploader(
+ config:,
+ uploader:,
+ remote_directory:,
+ enabled: true,
+ proxy_download: false,
+ background_upload: false,
+ direct_upload: false
+ )
+ allow(config).to receive(:enabled) { enabled }
+ allow(config).to receive(:proxy_download) { proxy_download }
+ allow(config).to receive(:background_upload) { background_upload }
+ allow(config).to receive(:direct_upload) { direct_upload }
+
+ return unless enabled
+
+ Fog.mock!
+
+ ::Fog::Storage.new(uploader.object_store_credentials).tap do |connection|
+ begin
+ connection.directories.create(key: remote_directory)
+ rescue Excon::Error::Conflict
+ end
+ end
+ end
+
+ def stub_artifacts_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store,
+ uploader: JobArtifactUploader,
+ remote_directory: 'artifacts',
+ **params)
+ end
+
+ def stub_lfs_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.lfs.object_store,
+ uploader: LfsObjectUploader,
+ remote_directory: 'lfs-objects',
+ **params)
+ end
+
+ def stub_uploads_object_storage(uploader = described_class, **params)
+ stub_object_storage_uploader(config: Gitlab.config.uploads.object_store,
+ uploader: uploader,
+ remote_directory: 'uploads',
+ **params)
+ end
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 01321989f01..f14e69b1041 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -225,7 +225,7 @@ module TestEnv
end
def repos_path
- Gitlab.config.repositories.storages.default['path']
+ Gitlab.config.repositories.storages.default.legacy_disk_path
end
def backup_path
diff --git a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
new file mode 100644
index 00000000000..8544fb62b5a
--- /dev/null
+++ b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
@@ -0,0 +1,118 @@
+require 'rake_helper'
+
+describe 'gitlab:artifacts namespace rake task' do
+ before(:context) do
+ Rake.application.rake_require 'tasks/gitlab/artifacts/migrate'
+ end
+
+ let(:object_storage_enabled) { false }
+
+ before do
+ stub_artifacts_object_storage(enabled: object_storage_enabled)
+ end
+
+ subject { run_rake_task('gitlab:artifacts:migrate') }
+
+ context 'legacy artifacts' do
+ describe 'migrate' do
+ let!(:build) { create(:ci_build, :legacy_artifacts, artifacts_file_store: store, artifacts_metadata_store: store) }
+
+ context 'when local storage is used' do
+ let(:store) { ObjectStorage::Store::LOCAL }
+
+ context 'and job does not have file store defined' do
+ let(:object_storage_enabled) { true }
+ let(:store) { nil }
+
+ it "migrates file to remote storage" do
+ subject
+
+ expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is defined' do
+ let(:object_storage_enabled) { true }
+
+ it "migrates file to remote storage" do
+ subject
+
+ expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is not defined' do
+ it "fails to migrate to remote storage" do
+ subject
+
+ expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::LOCAL)
+ expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
+
+ context 'when remote storage is used' do
+ let(:object_storage_enabled) { true }
+
+ let(:store) { ObjectStorage::Store::REMOTE }
+
+ it "file stays on remote storage" do
+ subject
+
+ expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+ end
+
+ context 'job artifacts' do
+ let!(:artifact) { create(:ci_job_artifact, :archive, file_store: store) }
+
+ context 'when local storage is used' do
+ let(:store) { ObjectStorage::Store::LOCAL }
+
+ context 'and job does not have file store defined' do
+ let(:object_storage_enabled) { true }
+ let(:store) { nil }
+
+ it "migrates file to remote storage" do
+ subject
+
+ expect(artifact.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is defined' do
+ let(:object_storage_enabled) { true }
+
+ it "migrates file to remote storage" do
+ subject
+
+ expect(artifact.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is not defined' do
+ it "fails to migrate to remote storage" do
+ subject
+
+ expect(artifact.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
+
+ context 'when remote storage is used' do
+ let(:object_storage_enabled) { true }
+ let(:store) { ObjectStorage::Store::REMOTE }
+
+ it "file stays on remote storage" do
+ subject
+
+ expect(artifact.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 168facd51a6..0d24782f317 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -195,14 +195,23 @@ describe 'gitlab:app namespace rake task' do
end
context 'multiple repository storages' do
- let(:gitaly_address) { Gitlab.config.repositories.storages.default.gitaly_address }
+ let(:storage_default) do
+ Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage'))
+ end
+ let(:test_second_storage) do
+ Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/custom_storage'))
+ end
let(:storages) do
{
- 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address },
- 'test_second_storage' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
+ 'default' => storage_default,
+ 'test_second_storage' => test_second_storage
}
end
+ before(:all) do
+ @default_storage_hash = Gitlab.config.repositories.storages.default.to_h
+ end
+
before do
# We only need a backup of the repositories for this test
stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb
index 9e746ceddd6..2bf873c923f 100644
--- a/spec/tasks/gitlab/cleanup_rake_spec.rb
+++ b/spec/tasks/gitlab/cleanup_rake_spec.rb
@@ -6,13 +6,16 @@ describe 'gitlab:cleanup rake tasks' do
end
describe 'cleanup' do
- let(:gitaly_address) { Gitlab.config.repositories.storages.default.gitaly_address }
let(:storages) do
{
- 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address }
+ 'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage'))
}
end
+ before(:all) do
+ @default_storage_hash = Gitlab.config.repositories.storages.default.to_h
+ end
+
before do
FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage'))
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb
index 9aebf7b0b4a..1efaecc63a5 100644
--- a/spec/tasks/gitlab/git_rake_spec.rb
+++ b/spec/tasks/gitlab/git_rake_spec.rb
@@ -1,10 +1,13 @@
require 'rake_helper'
describe 'gitlab:git rake tasks' do
+ before(:all) do
+ @default_storage_hash = Gitlab.config.repositories.storages.default.to_h
+ end
+
before do
Rake.application.rake_require 'tasks/gitlab/git'
-
- storages = { 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') } }
+ storages = { 'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage')) }
FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git'))
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 1f4053ff9ad..1e507c0236e 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -99,14 +99,14 @@ describe 'gitlab:gitaly namespace rake task' do
describe 'storage_config' do
it 'prints storage configuration in a TOML format' do
config = {
- 'default' => {
+ 'default' => Gitlab::GitalyClient::StorageSettings.new(
'path' => '/path/to/default',
'gitaly_address' => 'unix:/path/to/my.socket'
- },
- 'nfs_01' => {
+ ),
+ 'nfs_01' => Gitlab::GitalyClient::StorageSettings.new(
'path' => '/path/to/nfs_01',
'gitaly_address' => 'unix:/path/to/my.socket'
- }
+ )
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(config)
allow(Rails.env).to receive(:test?).and_return(false)
@@ -134,7 +134,7 @@ describe 'gitlab:gitaly namespace rake task' do
parsed_output = TomlRB.parse(expected_output)
config.each do |name, params|
- expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params['path'] })
+ expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params.legacy_disk_path })
end
end
end
diff --git a/spec/tasks/gitlab/lfs/migrate_rake_spec.rb b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
new file mode 100644
index 00000000000..66d1a192a96
--- /dev/null
+++ b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
@@ -0,0 +1,37 @@
+require 'rake_helper'
+
+describe 'gitlab:lfs namespace rake task' do
+ before :all do
+ Rake.application.rake_require 'tasks/gitlab/lfs/migrate'
+ end
+
+ describe 'migrate' do
+ let(:local) { ObjectStorage::Store::LOCAL }
+ let(:remote) { ObjectStorage::Store::REMOTE }
+ let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) }
+
+ def lfs_migrate
+ run_rake_task('gitlab:lfs:migrate')
+ end
+
+ context 'object storage disabled' do
+ before do
+ stub_lfs_object_storage(enabled: false)
+ end
+
+ it "doesn't migrate files" do
+ expect { lfs_migrate }.not_to change { lfs_object.reload.file_store }
+ end
+ end
+
+ context 'object storage enabled' do
+ before do
+ stub_lfs_object_storage
+ end
+
+ it 'migrates local file to object storage' do
+ expect { lfs_migrate }.to change { lfs_object.reload.file_store }.from(local).to(remote)
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index 65155cb044d..4a756c5742d 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -11,7 +11,7 @@ describe 'gitlab:shell rake tasks' do
it 'invokes create_hooks task' do
expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
- storages = Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
+ storages = Gitlab.config.repositories.storages.values.map(&:legacy_disk_path)
expect(Kernel).to receive(:system).with('bin/install', *storages).and_call_original
expect(Kernel).to receive(:system).with('bin/compile').and_call_original
diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
new file mode 100644
index 00000000000..b778d26060d
--- /dev/null
+++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
@@ -0,0 +1,28 @@
+require 'rake_helper'
+
+describe 'gitlab:uploads:migrate rake tasks' do
+ let!(:projects) { create_list(:project, 10, :with_avatar) }
+ let(:model_class) { Project }
+ let(:uploader_class) { AvatarUploader }
+ let(:mounted_as) { :avatar }
+ let(:batch_size) { 3 }
+
+ before do
+ stub_env('BATCH', batch_size.to_s)
+ stub_uploads_object_storage(uploader_class)
+ Rake.application.rake_require 'tasks/gitlab/uploads/migrate'
+
+ allow(ObjectStorage::MigrateUploadsWorker).to receive(:perform_async)
+ end
+
+ def run
+ args = [uploader_class.to_s, model_class.to_s, mounted_as].compact
+ run_rake_task("gitlab:uploads:migrate", *args)
+ end
+
+ it 'enqueue jobs in batch' do
+ expect(ObjectStorage::MigrateUploadsWorker).to receive(:enqueue!).exactly(4).times
+
+ run
+ end
+end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index 091ba824fc6..d302c14efb9 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -11,4 +11,26 @@ describe AttachmentUploader do
store_dir: %r[uploads/-/system/note/attachment/],
upload_path: %r[uploads/-/system/note/attachment/],
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/note/attachment/]
+
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[note/attachment/],
+ upload_path: %r[note/attachment/]
+ end
+
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
+ stub_uploads_object_storage
+ end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index bf9028c9260..b0468bc35ff 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe AvatarUploader do
- let(:model) { create(:user, :with_avatar) }
+ let(:model) { build_stubbed(:user) }
let(:uploader) { described_class.new(model, :avatar) }
let(:upload) { create(:upload, model: model) }
@@ -12,15 +12,28 @@ describe AvatarUploader do
upload_path: %r[uploads/-/system/user/avatar/],
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/avatar/]
- describe '#move_to_cache' do
- it 'is false' do
- expect(uploader.move_to_cache).to eq(false)
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[user/avatar/],
+ upload_path: %r[user/avatar/]
end
- describe '#move_to_store' do
- it 'is false' do
- expect(uploader.move_to_store).to eq(false)
+ context "with a file" do
+ let(:project) { create(:project, :with_avatar) }
+ let(:uploader) { project.avatar }
+ let(:upload) { uploader.upload }
+
+ before do
+ stub_uploads_object_storage
end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
end
end
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index bc024cd307c..68b7e24776d 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -36,6 +36,12 @@ describe FileMover do
it 'creates a new update record' do
expect { subject }.to change { Upload.count }.by(1)
end
+
+ it 'schedules a background migration' do
+ expect_any_instance_of(PersonalFileUploader).to receive(:schedule_background_upload).once
+
+ subject
+ end
end
context 'when update_markdown fails' do
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index b42ce982b27..db2810bbe1d 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -11,32 +11,41 @@ describe FileUploader do
shared_examples 'builds correct legacy storage paths' do
include_examples 'builds correct paths',
store_dir: %r{awesome/project/\h+},
+ upload_path: %r{\h+/<filename>},
absolute_path: %r{#{described_class.root}/awesome/project/secret/foo.jpg}
end
- shared_examples 'uses hashed storage' do
- context 'when rolled out attachments' do
- let(:project) { build_stubbed(:project, namespace: group, name: 'project') }
+ context 'legacy storage' do
+ it_behaves_like 'builds correct legacy storage paths'
- before do
- allow(project).to receive(:disk_path).and_return('ca/fe/fe/ed')
- end
+ context 'uses hashed storage' do
+ context 'when rolled out attachments' do
+ let(:project) { build_stubbed(:project, namespace: group, name: 'project') }
- it_behaves_like 'builds correct paths',
- store_dir: %r{ca/fe/fe/ed/\h+},
- absolute_path: %r{#{described_class.root}/ca/fe/fe/ed/secret/foo.jpg}
- end
+ include_examples 'builds correct paths',
+ store_dir: %r{@hashed/\h{2}/\h{2}/\h+},
+ upload_path: %r{\h+/<filename>}
+ end
- context 'when only repositories are rolled out' do
- let(:project) { build_stubbed(:project, namespace: group, name: 'project', storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) }
+ context 'when only repositories are rolled out' do
+ let(:project) { build_stubbed(:project, namespace: group, name: 'project', storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) }
- it_behaves_like 'builds correct legacy storage paths'
+ it_behaves_like 'builds correct legacy storage paths'
+ end
end
end
- context 'legacy storage' do
- it_behaves_like 'builds correct legacy storage paths'
- include_examples 'uses hashed storage'
+ context 'object store is remote' do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ # always use hashed storage path for remote uploads
+ it_behaves_like 'builds correct paths',
+ store_dir: %r{@hashed/\h{2}/\h{2}/\h+},
+ upload_path: %r{@hashed/\h{2}/\h{2}/\h+/\h+/<filename>}
end
describe 'initialize' do
@@ -78,6 +87,16 @@ describe FileUploader do
end
end
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')))
+ stub_uploads_object_storage
+ end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
+
describe '#upload=' do
let(:secret) { SecureRandom.hex }
let(:upload) { create(:upload, :issuable_upload, secret: secret, filename: 'file.txt') }
@@ -93,15 +112,5 @@ describe FileUploader do
uploader.upload = upload
end
-
- context 'uploader_context is empty' do
- it 'fallbacks to regex based extraction' do
- expect(upload).to receive(:uploader_context).and_return({})
-
- uploader.upload = upload
- expect(uploader.secret).to eq(secret)
- expect(uploader.instance_variable_get(:@identifier)).to eq('file.txt')
- end
- end
end
end
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
index 60e35dcf235..4fba122cce1 100644
--- a/spec/uploaders/gitlab_uploader_spec.rb
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -27,7 +27,7 @@ describe GitlabUploader do
describe '#file_cache_storage?' do
context 'when file storage is used' do
before do
- uploader_class.cache_storage(:file)
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::File }
end
it { is_expected.to be_file_cache_storage }
@@ -35,7 +35,7 @@ describe GitlabUploader do
context 'when is remote storage' do
before do
- uploader_class.cache_storage(:fog)
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::Fog }
end
it { is_expected.not_to be_file_cache_storage }
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index 5612ec7e661..42036d67f3d 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe JobArtifactUploader do
- let(:job_artifact) { create(:ci_job_artifact) }
+ let(:store) { described_class::Store::LOCAL }
+ let(:job_artifact) { create(:ci_job_artifact, file_store: store) }
let(:uploader) { described_class.new(job_artifact, :file) }
subject { uploader }
@@ -11,6 +12,17 @@ describe JobArtifactUploader do
cache_dir: %r[artifacts/tmp/cache],
work_dir: %r[artifacts/tmp/work]
+ context "object store is REMOTE" do
+ before do
+ stub_artifacts_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z]
+ end
+
describe '#open' do
subject { uploader.open }
@@ -36,6 +48,17 @@ describe JobArtifactUploader do
end
end
end
+
+ context 'when trace is stored in Object storage' do
+ before do
+ allow(uploader).to receive(:file_storage?) { false }
+ allow(uploader).to receive(:url) { 'http://object_storage.com/trace' }
+ end
+
+ it 'returns http io stream' do
+ is_expected.to be_a(Gitlab::Ci::Trace::HttpIO)
+ end
+ end
end
context 'file is stored in valid local_path' do
@@ -55,4 +78,14 @@ describe JobArtifactUploader do
it { is_expected.to include("/#{job_artifact.job_id}/#{job_artifact.id}/") }
it { is_expected.to end_with("ci_build_artifacts.zip") }
end
+
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/trace/sample_trace')))
+ stub_artifacts_object_storage
+ end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
end
diff --git a/spec/uploaders/legacy_artifact_uploader_spec.rb b/spec/uploaders/legacy_artifact_uploader_spec.rb
index 54c6a8b869b..eeb6fd90c9d 100644
--- a/spec/uploaders/legacy_artifact_uploader_spec.rb
+++ b/spec/uploaders/legacy_artifact_uploader_spec.rb
@@ -1,7 +1,8 @@
require 'rails_helper'
describe LegacyArtifactUploader do
- let(:job) { create(:ci_build) }
+ let(:store) { described_class::Store::LOCAL }
+ let(:job) { create(:ci_build, artifacts_file_store: store) }
let(:uploader) { described_class.new(job, :legacy_artifacts_file) }
let(:local_path) { described_class.root }
@@ -20,6 +21,17 @@ describe LegacyArtifactUploader do
cache_dir: %r[artifacts/tmp/cache],
work_dir: %r[artifacts/tmp/work]
+ context 'object store is remote' do
+ before do
+ stub_artifacts_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\d{4}_\d{1,2}/\d+/\d+\z]
+ end
+
describe '#filename' do
# we need to use uploader, as this makes to use mounter
# which initialises uploader.file object
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index 6ebc885daa8..a2fb3886610 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -11,4 +11,62 @@ describe LfsObjectUploader do
store_dir: %r[\h{2}/\h{2}],
cache_dir: %r[/lfs-objects/tmp/cache],
work_dir: %r[/lfs-objects/tmp/work]
+
+ context "object store is REMOTE" do
+ before do
+ stub_lfs_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}]
+ end
+
+ describe 'migration to object storage' do
+ context 'with object storage disabled' do
+ it "is skipped" do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ lfs_object
+ end
+ end
+
+ context 'with object storage enabled' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
+
+ it 'is scheduled to run after creation' do
+ expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with(described_class.name, 'LfsObject', :file, kind_of(Numeric))
+
+ lfs_object
+ end
+ end
+ end
+
+ describe 'remote file' do
+ let(:remote) { described_class::Store::REMOTE }
+ let(:lfs_object) { create(:lfs_object, file_store: remote) }
+
+ context 'with object storage enabled' do
+ before do
+ stub_lfs_object_storage
+ end
+
+ it 'can store file remotely' do
+ allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
+
+ store_file(lfs_object)
+
+ expect(lfs_object.file_store).to eq remote
+ expect(lfs_object.file.path).not_to be_blank
+ end
+ end
+ end
+
+ def store_file(lfs_object)
+ lfs_object.file = fixture_file_upload(Rails.root.join("spec/fixtures/dk.png"), "`/png")
+ lfs_object.save!
+ end
end
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index 24a2fc0f72e..a8ba01d70b8 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -13,4 +13,26 @@ describe NamespaceFileUploader do
store_dir: %r[uploads/-/system/namespace/\d+],
upload_path: IDENTIFIER,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{IDENTIFIER}]
+
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[namespace/\d+/\h+],
+ upload_path: IDENTIFIER
+ end
+
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
+ stub_uploads_object_storage
+ end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
new file mode 100644
index 00000000000..1d406c71955
--- /dev/null
+++ b/spec/uploaders/object_storage_spec.rb
@@ -0,0 +1,630 @@
+require 'rails_helper'
+require 'carrierwave/storage/fog'
+
+class Implementation < GitlabUploader
+ include ObjectStorage::Concern
+ include ::RecordsUploads::Concern
+ prepend ::ObjectStorage::Extension::RecordsUploads
+
+ storage_options Gitlab.config.uploads
+
+ private
+
+ # user/:id
+ def dynamic_segment
+ File.join(model.class.to_s.underscore, model.id.to_s)
+ end
+end
+
+describe ObjectStorage do
+ let(:uploader_class) { Implementation }
+ let(:object) { build_stubbed(:user) }
+ let(:uploader) { uploader_class.new(object, :file) }
+
+ describe '#object_store=' do
+ before do
+ allow(uploader_class).to receive(:object_store_enabled?).and_return(true)
+ end
+
+ it "reload the local storage" do
+ uploader.object_store = described_class::Store::LOCAL
+ expect(uploader.file_storage?).to be_truthy
+ end
+
+ it "reload the REMOTE storage" do
+ uploader.object_store = described_class::Store::REMOTE
+ expect(uploader.file_storage?).to be_falsey
+ end
+
+ context 'object_store is Store::LOCAL' do
+ before do
+ uploader.object_store = described_class::Store::LOCAL
+ end
+
+ describe '#store_dir' do
+ it 'is the composition of (base_dir, dynamic_segment)' do
+ expect(uploader.store_dir).to start_with("uploads/-/system/user/")
+ end
+ end
+ end
+
+ context 'object_store is Store::REMOTE' do
+ before do
+ uploader.object_store = described_class::Store::REMOTE
+ end
+
+ describe '#store_dir' do
+ it 'is the composition of (dynamic_segment)' do
+ expect(uploader.store_dir).to start_with("user/")
+ end
+ end
+ end
+ end
+
+ describe '#object_store' do
+ it "delegates to <mount>_store on model" do
+ expect(object).to receive(:file_store)
+
+ uploader.object_store
+ end
+
+ context 'when store is null' do
+ before do
+ expect(object).to receive(:file_store).and_return(nil)
+ end
+
+ it "returns Store::LOCAL" do
+ expect(uploader.object_store).to eq(described_class::Store::LOCAL)
+ end
+ end
+
+ context 'when value is set' do
+ before do
+ expect(object).to receive(:file_store).and_return(described_class::Store::REMOTE)
+ end
+
+ it "returns the given value" do
+ expect(uploader.object_store).to eq(described_class::Store::REMOTE)
+ end
+ end
+ end
+
+ describe '#file_cache_storage?' do
+ context 'when file storage is used' do
+ before do
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::File }
+ end
+
+ it { expect(uploader).to be_file_cache_storage }
+ end
+
+ context 'when is remote storage' do
+ before do
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::Fog }
+ end
+
+ it { expect(uploader).not_to be_file_cache_storage }
+ end
+ end
+
+ # this means the model shall include
+ # include RecordsUpload::Concern
+ # prepend ObjectStorage::Extension::RecordsUploads
+ # the object_store persistence is delegated to the `Upload` model.
+ #
+ context 'when persist_object_store? is false' do
+ let(:object) { create(:project, :with_avatar) }
+ let(:uploader) { object.avatar }
+
+ it { expect(object).to be_a(Avatarable) }
+ it { expect(uploader.persist_object_store?).to be_falsey }
+
+ describe 'delegates the object_store logic to the `Upload` model' do
+ it 'sets @upload to the found `upload`' do
+ expect(uploader.upload).to eq(uploader.upload)
+ end
+
+ it 'sets @object_store to the `Upload` value' do
+ expect(uploader.object_store).to eq(uploader.upload.store)
+ end
+ end
+
+ describe '#migrate!' do
+ let(:new_store) { ObjectStorage::Store::REMOTE }
+
+ before do
+ stub_uploads_object_storage(uploader: AvatarUploader)
+ end
+
+ subject { uploader.migrate!(new_store) }
+
+ it 'persist @object_store to the recorded upload' do
+ subject
+
+ expect(uploader.upload.store).to eq(new_store)
+ end
+
+ describe 'fails' do
+ it 'is handled gracefully' do
+ store = uploader.object_store
+ expect_any_instance_of(Upload).to receive(:save!).and_raise("An error")
+
+ expect { subject }.to raise_error("An error")
+ expect(uploader.exists?).to be_truthy
+ expect(uploader.upload.store).to eq(store)
+ end
+ end
+ end
+ end
+
+ # this means the model holds an <mounted_as>_store attribute directly
+ # and do not delegate the object_store persistence to the `Upload` model.
+ #
+ context 'persist_object_store? is true' do
+ context 'when using JobArtifactsUploader' do
+ let(:store) { described_class::Store::LOCAL }
+ let(:object) { create(:ci_job_artifact, :archive, file_store: store) }
+ let(:uploader) { object.file }
+
+ context 'checking described_class' do
+ it "uploader include described_class::Concern" do
+ expect(uploader).to be_a(described_class::Concern)
+ end
+ end
+
+ describe '#use_file' do
+ context 'when file is stored locally' do
+ it "calls a regular path" do
+ expect { |b| uploader.use_file(&b) }.not_to yield_with_args(%r[tmp/cache])
+ end
+ end
+
+ context 'when file is stored remotely' do
+ let(:store) { described_class::Store::REMOTE }
+
+ before do
+ stub_artifacts_object_storage
+ end
+
+ it "calls a cache path" do
+ expect { |b| uploader.use_file(&b) }.to yield_with_args(%r[tmp/cache])
+ end
+ end
+ end
+
+ describe '#migrate!' do
+ subject { uploader.migrate!(new_store) }
+
+ shared_examples "updates the underlying <mounted>_store" do
+ it do
+ subject
+
+ expect(object.file_store).to eq(new_store)
+ end
+ end
+
+ context 'when using the same storage' do
+ let(:new_store) { store }
+
+ it "to not migrate the storage" do
+ subject
+
+ expect(uploader).not_to receive(:store!)
+ expect(uploader.object_store).to eq(store)
+ end
+ end
+
+ context 'when migrating to local storage' do
+ let(:store) { described_class::Store::REMOTE }
+ let(:new_store) { described_class::Store::LOCAL }
+
+ before do
+ stub_artifacts_object_storage
+ end
+
+ include_examples "updates the underlying <mounted>_store"
+
+ it "local file does not exist" do
+ expect(File.exist?(uploader.path)).to eq(false)
+ end
+
+ it "remote file exist" do
+ expect(uploader.file.exists?).to be_truthy
+ end
+
+ it "does migrate the file" do
+ subject
+
+ expect(uploader.object_store).to eq(new_store)
+ expect(File.exist?(uploader.path)).to eq(true)
+ end
+ end
+
+ context 'when migrating to remote storage' do
+ let(:new_store) { described_class::Store::REMOTE }
+ let!(:current_path) { uploader.path }
+
+ it "file does exist" do
+ expect(File.exist?(current_path)).to eq(true)
+ end
+
+ context 'when storage is disabled' do
+ before do
+ stub_artifacts_object_storage(enabled: false)
+ end
+
+ it "to raise an error" do
+ expect { subject }.to raise_error(/Object Storage is not enabled/)
+ end
+ end
+
+ context 'when credentials are set' do
+ before do
+ stub_artifacts_object_storage
+ end
+
+ include_examples "updates the underlying <mounted>_store"
+
+ it "does migrate the file" do
+ subject
+
+ expect(uploader.object_store).to eq(new_store)
+ end
+
+ it "does delete original file" do
+ subject
+
+ expect(File.exist?(current_path)).to eq(false)
+ end
+
+ context 'when subject save fails' do
+ before do
+ expect(uploader).to receive(:persist_object_store!).and_raise(RuntimeError, "exception")
+ end
+
+ it "original file is not removed" do
+ expect { subject }.to raise_error(/exception/)
+
+ expect(File.exist?(current_path)).to eq(true)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '#fog_directory' do
+ let(:remote_directory) { 'directory' }
+
+ before do
+ allow(uploader_class).to receive(:options) do
+ double(object_store: double(remote_directory: remote_directory))
+ end
+ end
+
+ subject { uploader.fog_directory }
+
+ it { is_expected.to eq(remote_directory) }
+ end
+
+ describe '#fog_credentials' do
+ let(:connection) { Settingslogic.new("provider" => "AWS") }
+
+ before do
+ allow(uploader_class).to receive(:options) do
+ double(object_store: double(connection: connection))
+ end
+ end
+
+ subject { uploader.fog_credentials }
+
+ it { is_expected.to eq(provider: 'AWS') }
+ end
+
+ describe '#fog_public' do
+ subject { uploader.fog_public }
+
+ it { is_expected.to eq(false) }
+ end
+
+ describe '.workhorse_authorize' do
+ subject { uploader_class.workhorse_authorize }
+
+ before do
+ # ensure that we use regular Fog libraries
+ # other tests might call `Fog.mock!` and
+ # it will make tests to fail
+ Fog.unmock!
+ end
+
+ shared_examples 'uses local storage' do
+ it "returns temporary path" do
+ is_expected.to have_key(:TempPath)
+
+ expect(subject[:TempPath]).to start_with(uploader_class.root)
+ expect(subject[:TempPath]).to include(described_class::TMP_UPLOAD_PATH)
+ end
+
+ it "does not return remote store" do
+ is_expected.not_to have_key('RemoteObject')
+ end
+ end
+
+ shared_examples 'uses remote storage' do
+ it "returns remote store" do
+ is_expected.to have_key(:RemoteObject)
+
+ expect(subject[:RemoteObject]).to have_key(:ID)
+ expect(subject[:RemoteObject]).to have_key(:GetURL)
+ expect(subject[:RemoteObject]).to have_key(:DeleteURL)
+ expect(subject[:RemoteObject]).to have_key(:StoreURL)
+ expect(subject[:RemoteObject][:GetURL]).to include(described_class::TMP_UPLOAD_PATH)
+ expect(subject[:RemoteObject][:DeleteURL]).to include(described_class::TMP_UPLOAD_PATH)
+ expect(subject[:RemoteObject][:StoreURL]).to include(described_class::TMP_UPLOAD_PATH)
+ end
+
+ it "does not return local store" do
+ is_expected.not_to have_key('TempPath')
+ end
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:enabled) { false }
+ end
+
+ it_behaves_like 'uses local storage'
+ end
+
+ context 'when object storage is enabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:enabled) { true }
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:direct_upload) { true }
+ end
+
+ context 'uses AWS' do
+ before do
+ expect(uploader_class).to receive(:object_store_credentials) do
+ { provider: "AWS",
+ aws_access_key_id: "AWS_ACCESS_KEY_ID",
+ aws_secret_access_key: "AWS_SECRET_ACCESS_KEY",
+ region: "eu-central-1" }
+ end
+ end
+
+ it_behaves_like 'uses remote storage' do
+ let(:storage_url) { "https://uploads.s3-eu-central-1.amazonaws.com/" }
+
+ it 'returns links for S3' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+
+ context 'uses Google' do
+ before do
+ expect(uploader_class).to receive(:object_store_credentials) do
+ { provider: "Google",
+ google_storage_access_key_id: 'ACCESS_KEY_ID',
+ google_storage_secret_access_key: 'SECRET_ACCESS_KEY' }
+ end
+ end
+
+ it_behaves_like 'uses remote storage' do
+ let(:storage_url) { "https://storage.googleapis.com/uploads/" }
+
+ it 'returns links for Google Cloud' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+
+ context 'uses GDK/minio' do
+ before do
+ expect(uploader_class).to receive(:object_store_credentials) do
+ { provider: "AWS",
+ aws_access_key_id: "AWS_ACCESS_KEY_ID",
+ aws_secret_access_key: "AWS_SECRET_ACCESS_KEY",
+ endpoint: 'http://127.0.0.1:9000',
+ path_style: true,
+ region: "gdk" }
+ end
+ end
+
+ it_behaves_like 'uses remote storage' do
+ let(:storage_url) { "http://127.0.0.1:9000/uploads/" }
+
+ it 'returns links for S3' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:direct_upload) { false }
+ end
+
+ it_behaves_like 'uses local storage'
+ end
+ end
+ end
+
+ describe '#store_workhorse_file!' do
+ subject do
+ uploader.store_workhorse_file!(params, :file)
+ end
+
+ context 'when local file is used' do
+ context 'when valid file is used' do
+ let(:target_path) do
+ File.join(uploader_class.root, uploader_class::TMP_UPLOAD_PATH)
+ end
+
+ before do
+ FileUtils.mkdir_p(target_path)
+ end
+
+ context 'when no filename is specified' do
+ let(:params) do
+ { "file.path" => "test/file" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing filename/)
+ end
+ end
+
+ context 'when invalid file is specified' do
+ let(:file_path) do
+ File.join(target_path, "..", "test.file")
+ end
+
+ before do
+ FileUtils.touch(file_path)
+ end
+
+ let(:params) do
+ { "file.path" => file_path,
+ "file.name" => "my_file.txt" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file path/)
+ end
+ end
+
+ context 'when filename is specified' do
+ let(:params) do
+ { "file.path" => tmp_file,
+ "file.name" => "my_file.txt" }
+ end
+
+ let(:tmp_file) { Tempfile.new('filename', target_path) }
+
+ before do
+ FileUtils.touch(tmp_file)
+ end
+
+ after do
+ FileUtils.rm_f(tmp_file)
+ end
+
+ it 'succeeds' do
+ expect { subject }.not_to raise_error
+
+ expect(uploader).to be_exists
+ end
+
+ it 'proper path is being used' do
+ subject
+
+ expect(uploader.path).to start_with(uploader_class.root)
+ expect(uploader.path).to end_with("my_file.txt")
+ end
+
+ it 'source file to not exist' do
+ subject
+
+ expect(File.exist?(tmp_file.path)).to be_falsey
+ end
+ end
+ end
+ end
+
+ context 'when remote file is used' do
+ let!(:fog_connection) do
+ stub_uploads_object_storage(uploader_class)
+ end
+
+ context 'when valid file is used' do
+ context 'when no filename is specified' do
+ let(:params) do
+ { "file.remote_id" => "test/123123" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing filename/)
+ end
+ end
+
+ context 'when invalid file is specified' do
+ let(:params) do
+ { "file.remote_id" => "../test/123123",
+ "file.name" => "my_file.txt" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file path/)
+ end
+ end
+
+ context 'when non existing file is specified' do
+ let(:params) do
+ { "file.remote_id" => "test/12312300",
+ "file.name" => "my_file.txt" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing file/)
+ end
+ end
+
+ context 'when filename is specified' do
+ let(:params) do
+ { "file.remote_id" => "test/123123",
+ "file.name" => "my_file.txt" }
+ end
+
+ let!(:fog_file) do
+ fog_connection.directories.get('uploads').files.create(
+ key: 'tmp/upload/test/123123',
+ body: 'content'
+ )
+ end
+
+ it 'succeeds' do
+ expect { subject }.not_to raise_error
+
+ expect(uploader).to be_exists
+ end
+
+ it 'path to not be temporary' do
+ subject
+
+ expect(uploader.path).not_to be_nil
+ expect(uploader.path).not_to include('tmp/upload')
+ expect(uploader.url).to include('/my_file.txt')
+ end
+
+ it 'url is used' do
+ subject
+
+ expect(uploader.url).not_to be_nil
+ expect(uploader.url).to include('/my_file.txt')
+ end
+ end
+ end
+ end
+
+ context 'when no file is used' do
+ let(:params) { {} }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file/)
+ end
+ end
+ end
+end
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index ed1fba6edda..c70521d90dc 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -14,6 +14,18 @@ describe PersonalFileUploader do
upload_path: IDENTIFIER,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{IDENTIFIER}]
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[\d+/\h+],
+ upload_path: IDENTIFIER
+ end
+
describe '#to_h' do
before do
subject.instance_variable_set(:@secret, 'secret')
@@ -30,4 +42,14 @@ describe PersonalFileUploader do
)
end
end
+
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
+ stub_uploads_object_storage
+ end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
end
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/projects/ci/lints/show.html.haml_spec.rb
index 7724d54c569..2f0cd38c14a 100644
--- a/spec/views/ci/lints/show.html.haml_spec.rb
+++ b/spec/views/projects/ci/lints/show.html.haml_spec.rb
@@ -1,11 +1,13 @@
require 'spec_helper'
-describe 'ci/lints/show' do
+describe 'projects/ci/lints/show' do
include Devise::Test::ControllerHelpers
+ let(:project) { create(:project, :repository) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
describe 'XSS protection' do
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
before do
+ assign(:project, project)
assign(:status, true)
assign(:builds, config_processor.builds)
assign(:stages, config_processor.stages)
@@ -47,22 +49,21 @@ describe 'ci/lints/show' do
end
end
- let(:content) do
- {
- build_template: {
- script: './build.sh',
- tags: ['dotnet'],
- only: ['test@dude/repo'],
- except: ['deploy'],
- environment: 'testing'
+ context 'when the content is valid' do
+ let(:content) do
+ {
+ build_template: {
+ script: './build.sh',
+ tags: ['dotnet'],
+ only: ['test@dude/repo'],
+ except: ['deploy'],
+ environment: 'testing'
+ }
}
- }
- end
-
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
+ end
- context 'when the content is valid' do
before do
+ assign(:project, project)
assign(:status, true)
assign(:builds, config_processor.builds)
assign(:stages, config_processor.stages)
@@ -82,6 +83,7 @@ describe 'ci/lints/show' do
context 'when the content is invalid' do
before do
+ assign(:project, project)
assign(:status, false)
assign(:error, 'Undefined error')
end
diff --git a/spec/views/projects/diffs/_stats.html.haml_spec.rb b/spec/views/projects/diffs/_stats.html.haml_spec.rb
new file mode 100644
index 00000000000..c7d2f85747c
--- /dev/null
+++ b/spec/views/projects/diffs/_stats.html.haml_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'projects/diffs/_stats.html.haml' do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
+
+ def render_view
+ render partial: "projects/diffs/stats", locals: { diff_files: commit.diffs.diff_files }
+ end
+
+ context 'when the commit contains several changes' do
+ it 'uses plural for additions' do
+ render_view
+
+ expect(rendered).to have_text('additions')
+ end
+
+ it 'uses plural for deletions' do
+ render_view
+ end
+ end
+
+ context 'when the commit contains no addition and no deletions' do
+ let(:commit) { project.commit('4cd80ccab63c82b4bad16faa5193fbd2aa06df40') }
+
+ it 'uses plural for additions' do
+ render_view
+
+ expect(rendered).to have_text('additions')
+ end
+
+ it 'uses plural for deletions' do
+ render_view
+
+ expect(rendered).to have_text('deletions')
+ end
+ end
+
+ context 'when the commit contains exactly one addition and one deletion' do
+ let(:commit) { project.commit('08f22f255f082689c0d7d39d19205085311542bc') }
+
+ it 'uses singular for additions' do
+ render_view
+
+ expect(rendered).to have_text('addition')
+ expect(rendered).not_to have_text('additions')
+ end
+
+ it 'uses singular for deletions' do
+ render_view
+
+ expect(rendered).to have_text('deletion')
+ expect(rendered).not_to have_text('deletions')
+ end
+ end
+end
diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb
new file mode 100644
index 00000000000..85167bca115
--- /dev/null
+++ b/spec/views/projects/services/_form.haml_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe 'projects/services/_form' do
+ let(:project) { create(:redmine_project) }
+ let(:user) { create(:admin) }
+
+ before do
+ assign(:project, project)
+
+ allow(controller).to receive(:current_user).and_return(user)
+
+ allow(view).to receive_messages(current_user: user,
+ can?: true,
+ current_application_settings: Gitlab::CurrentSettings.current_application_settings)
+ end
+
+ context 'commit_events and merge_request_events' do
+ before do
+ assign(:service, project.redmine_service)
+ end
+
+ it 'display merge_request_events and commit_events descriptions' do
+ allow(RedmineService).to receive(:supported_events).and_return(%w(commit merge_request))
+
+ render
+
+ expect(rendered).to have_content('Event will be triggered when a commit is created/updated')
+ expect(rendered).to have_content('Event will be triggered when a merge request is created/updated/merged')
+ end
+
+ context 'when service is JIRA' do
+ let(:project) { create(:jira_project) }
+
+ before do
+ assign(:service, project.jira_service)
+ end
+
+ it 'display merge_request_events and commit_events descriptions' do
+ render
+
+ expect(rendered).to have_content('JIRA comments will be created when an issue gets referenced in a commit.')
+ expect(rendered).to have_content('JIRA comments will be created when an issue gets referenced in a merge request.')
+ end
+ end
+ end
+end
diff --git a/spec/workers/object_storage_upload_worker_spec.rb b/spec/workers/object_storage_upload_worker_spec.rb
new file mode 100644
index 00000000000..32ddcbe9757
--- /dev/null
+++ b/spec/workers/object_storage_upload_worker_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe ObjectStorageUploadWorker do
+ let(:local) { ObjectStorage::Store::LOCAL }
+ let(:remote) { ObjectStorage::Store::REMOTE }
+
+ def perform
+ described_class.perform_async(uploader_class.name, subject_class, file_field, subject_id)
+ end
+
+ context 'for LFS' do
+ let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) }
+ let(:uploader_class) { LfsObjectUploader }
+ let(:subject_class) { LfsObject }
+ let(:file_field) { :file }
+ let(:subject_id) { lfs_object.id }
+
+ context 'when object storage is enabled' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
+
+ it 'uploads object to storage' do
+ expect { perform }.to change { lfs_object.reload.file_store }.from(local).to(remote)
+ end
+
+ context 'when background upload is disabled' do
+ before do
+ allow(Gitlab.config.lfs.object_store).to receive(:background_upload) { false }
+ end
+
+ it 'is skipped' do
+ expect { perform }.not_to change { lfs_object.reload.file_store }
+ end
+ end
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ stub_lfs_object_storage(enabled: false)
+ end
+
+ it "doesn't migrate files" do
+ perform
+
+ expect(lfs_object.reload.file_store).to eq(local)
+ end
+ end
+ end
+
+ context 'for legacy artifacts' do
+ let(:build) { create(:ci_build, :legacy_artifacts) }
+ let(:uploader_class) { LegacyArtifactUploader }
+ let(:subject_class) { Ci::Build }
+ let(:file_field) { :artifacts_file }
+ let(:subject_id) { build.id }
+
+ context 'when local storage is used' do
+ let(:store) { local }
+
+ context 'and remote storage is defined' do
+ before do
+ stub_artifacts_object_storage(background_upload: true)
+ end
+
+ it "migrates file to remote storage" do
+ perform
+
+ expect(build.reload.artifacts_file_store).to eq(remote)
+ end
+
+ context 'for artifacts_metadata' do
+ let(:file_field) { :artifacts_metadata }
+
+ it 'migrates metadata to remote storage' do
+ perform
+
+ expect(build.reload.artifacts_metadata_store).to eq(remote)
+ end
+ end
+ end
+ end
+ end
+
+ context 'for job artifacts' do
+ let(:artifact) { create(:ci_job_artifact, :archive) }
+ let(:uploader_class) { JobArtifactUploader }
+ let(:subject_class) { Ci::JobArtifact }
+ let(:file_field) { :file }
+ let(:subject_id) { artifact.id }
+
+ context 'when local storage is used' do
+ let(:store) { local }
+
+ context 'and remote storage is defined' do
+ before do
+ stub_artifacts_object_storage(background_upload: true)
+ end
+
+ it "migrates file to remote storage" do
+ perform
+
+ expect(artifact.reload.file_store).to eq(remote)
+ end
+ end
+ end
+ end
+end