summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2016-06-15 15:43:12 +0800
committerLin Jen-Shin <godfat@godfat.org>2016-06-15 15:43:12 +0800
commite75391889e8bb13f9ead60eac88ffac5d9081f78 (patch)
tree10fd092d4011a923cfb8dd79d469061d3fb0aa4a /spec
parent8c0b619d40e4d113ef592f4ae7a4b6afa320f225 (diff)
parentbf4455d14659f1fde6391164b38310d361bf407d (diff)
downloadgitlab-ce-e75391889e8bb13f9ead60eac88ffac5d9081f78.tar.gz
Merge branch 'master' into new-issue-by-email
* master: (1246 commits) Update CHANGELOG Update tests to make it work with Turbolinks approach Use Turbolink instead of ajax Reinitialize checkboxes to toggle event bindings Turn off handlers before binding events Removed console.log Uses outerWidth instead of width Revert "Added API endpoint for Sidekiq Metrics" Added API endpoint for Sidekiq Metrics Added CHANGELOG entry for allocations Gem/name fix Filter out classes without names in the sampler Update the allocations Gem to 1.0.5 Put all sidebar icons in fixed width container Instrument private/protected methods Fix Ci::Build#artifacts_expire_in= when assigning invalid duration Fix grammar and syntax Update CI API docs UI and copywriting improvements Factorize members mails into a new Emails::Members module Factorize access request routes into a new :access_requestable route concern Factorize #request_access and #approve_access_request into a new AccessRequestActions controller concern ...
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/admin/projects_controller_spec.rb2
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb198
-rw-r--r--spec/controllers/groups_controller_spec.rb12
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb1
-rw-r--r--spec/controllers/import/fogbugz_controller_spec.rb1
-rw-r--r--spec/controllers/import/github_controller_spec.rb1
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb1
-rw-r--r--spec/controllers/import/gitorious_controller_spec.rb1
-rw-r--r--spec/controllers/import/google_code_controller_spec.rb1
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb29
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb14
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb4
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb4
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb4
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb37
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb53
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb97
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb36
-rw-r--r--spec/controllers/projects/notification_settings_controller_spec.rb14
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb251
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb4
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb5
-rw-r--r--spec/controllers/projects_controller_spec.rb45
-rw-r--r--spec/controllers/registrations_controller_spec.rb6
-rw-r--r--spec/controllers/sessions_controller_spec.rb55
-rw-r--r--spec/factories/award_emoji.rb12
-rw-r--r--spec/factories/ci/builds.rb4
-rw-r--r--spec/factories/ci/commits.rb10
-rw-r--r--spec/factories/commit_statuses.rb4
-rw-r--r--spec/factories/notes.rb26
-rw-r--r--spec/factories/projects.rb6
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/factories/u2f_registrations.rb8
-rw-r--r--spec/factories/users.rb14
-rw-r--r--spec/factories/wiki_pages.rb2
-rw-r--r--spec/factories_spec.rb4
-rw-r--r--spec/features/admin/admin_builds_spec.rb26
-rw-r--r--spec/features/admin/admin_runners_spec.rb6
-rw-r--r--spec/features/admin/admin_users_spec.rb20
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb51
-rw-r--r--spec/features/builds_spec.rb208
-rw-r--r--spec/features/commits_spec.rb66
-rw-r--r--spec/features/container_registry_spec.rb44
-rw-r--r--spec/features/dashboard/datetime_on_tooltips_spec.rb46
-rw-r--r--spec/features/groups/members/owner_manages_access_requests_spec.rb48
-rw-r--r--spec/features/groups/members/user_requests_access_spec.rb48
-rw-r--r--spec/features/issues/award_emoji_spec.rb2
-rw-r--r--spec/features/issues/award_spec.rb49
-rw-r--r--spec/features/issues/bulk_assigment_labels_spec.rb213
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb50
-rw-r--r--spec/features/issues/filter_issues_spec.rb163
-rw-r--r--spec/features/issues/move_spec.rb16
-rw-r--r--spec/features/issues/note_polling_spec.rb5
-rw-r--r--spec/features/issues/todo_spec.rb33
-rw-r--r--spec/features/issues/update_issues_spec.rb23
-rw-r--r--spec/features/issues_spec.rb109
-rw-r--r--spec/features/login_spec.rb36
-rw-r--r--spec/features/markdown_spec.rb21
-rw-r--r--spec/features/merge_requests/award_spec.rb49
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb58
-rw-r--r--spec/features/merge_requests/merge_when_build_succeeds_spec.rb8
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb105
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb9
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb33
-rw-r--r--spec/features/participants_autocomplete_spec.rb6
-rw-r--r--spec/features/pipelines_spec.rb189
-rw-r--r--spec/features/profiles/preferences_spec.rb8
-rw-r--r--spec/features/projects/badges/list_spec.rb5
-rw-r--r--spec/features/projects/commit/builds_spec.rb6
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb1
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb30
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb8
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb4
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb87
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb115
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb47
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb54
-rw-r--r--spec/features/projects/shortcuts_spec.rb (renamed from spec/features/project/shortcuts_spec.rb)4
-rw-r--r--spec/features/runners_spec.rb37
-rw-r--r--spec/features/security/project/public_access_spec.rb4
-rw-r--r--spec/features/tags/master_updates_tag_spec.rb2
-rw-r--r--spec/features/task_lists_spec.rb5
-rw-r--r--spec/features/todos/target_state_spec.rb65
-rw-r--r--spec/features/todos/todos_spec.rb36
-rw-r--r--spec/features/u2f_spec.rb239
-rw-r--r--spec/features/variables_spec.rb61
-rw-r--r--spec/fixtures/container_registry/config_blob.json1
-rw-r--r--spec/fixtures/container_registry/tag_manifest.json1
-rw-r--r--spec/fixtures/markdown.md.erb25
-rw-r--r--spec/helpers/auth_helper_spec.rb2
-rw-r--r--spec/helpers/ci_status_helper_spec.rb4
-rw-r--r--spec/helpers/diff_helper_spec.rb4
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb3
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb79
-rw-r--r--spec/helpers/issues_helper_spec.rb27
-rw-r--r--spec/helpers/members_helper_spec.rb72
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb4
-rw-r--r--spec/helpers/projects_helper_spec.rb10
-rw-r--r--spec/javascripts/awards_handler_spec.js.coffee201
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js.coffee22
-rw-r--r--spec/javascripts/fixtures/awards_handler.html.haml52
-rw-r--r--spec/javascripts/fixtures/behaviors/quick_submit.html.haml2
-rw-r--r--spec/javascripts/fixtures/emoji_menu.coffee957
-rw-r--r--spec/javascripts/fixtures/right_sidebar.html.haml13
-rw-r--r--spec/javascripts/fixtures/u2f/authenticate.html.haml1
-rw-r--r--spec/javascripts/fixtures/u2f/register.html.haml1
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js (renamed from spec/javascripts/stat_graph_contributors_graph_spec.js)2
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js (renamed from spec/javascripts/stat_graph_contributors_util_spec.js)14
-rw-r--r--spec/javascripts/graphs/stat_graph_spec.js (renamed from spec/javascripts/stat_graph_spec.js)2
-rw-r--r--spec/javascripts/new_branch_spec.js.coffee2
-rw-r--r--spec/javascripts/project_title_spec.js.coffee1
-rw-r--r--spec/javascripts/right_sidebar_spec.js.coffee69
-rw-r--r--spec/javascripts/u2f/authenticate_spec.coffee52
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js.coffee15
-rw-r--r--spec/javascripts/u2f/register_spec.js.coffee57
-rw-r--r--spec/lib/banzai/filter/commit_range_reference_filter_spec.rb15
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb15
-rw-r--r--spec/lib/banzai/filter/inline_diff_filter_spec.rb68
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb25
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb15
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb145
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb50
-rw-r--r--spec/lib/banzai/filter/reference_filter_spec.rb45
-rw-r--r--spec/lib/banzai/filter/reference_gatherer_filter_spec.rb87
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb15
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb50
-rw-r--r--spec/lib/banzai/filter/wiki_link_filter_spec.rb85
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb108
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb237
-rw-r--r--spec/lib/banzai/reference_parser/commit_parser_spec.rb113
-rw-r--r--spec/lib/banzai/reference_parser/commit_range_parser_spec.rb120
-rw-r--r--spec/lib/banzai/reference_parser/external_issue_parser_spec.rb62
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb79
-rw-r--r--spec/lib/banzai/reference_parser/label_parser_spec.rb31
-rw-r--r--spec/lib/banzai/reference_parser/merge_request_parser_spec.rb30
-rw-r--r--spec/lib/banzai/reference_parser/milestone_parser_spec.rb31
-rw-r--r--spec/lib/banzai/reference_parser/snippet_parser_spec.rb31
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb189
-rw-r--r--spec/lib/ci/ansi2html_spec.rb9
-rw-r--r--spec/lib/ci/charts_spec.rb13
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb55
-rw-r--r--spec/lib/container_registry/blob_spec.rb61
-rw-r--r--spec/lib/container_registry/registry_spec.rb28
-rw-r--r--spec/lib/container_registry/repository_spec.rb65
-rw-r--r--spec/lib/container_registry/tag_spec.rb89
-rw-r--r--spec/lib/disable_email_interceptor_spec.rb4
-rw-r--r--spec/lib/gitlab/akismet_helper_spec.rb4
-rw-r--r--spec/lib/gitlab/auth_spec.rb56
-rw-r--r--spec/lib/gitlab/award_emoji_spec.rb (renamed from spec/lib/award_emoji_spec.rb)8
-rw-r--r--spec/lib/gitlab/backend/grack_auth_spec.rb209
-rw-r--r--spec/lib/gitlab/badge/build_spec.rb26
-rw-r--r--spec/lib/gitlab/bitbucket_import/client_spec.rb6
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/loader_spec.rb50
-rw-r--r--spec/lib/gitlab/ci/config/node/configurable_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/config/node/factory_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/node/global_spec.rb104
-rw-r--r--spec/lib/gitlab/ci/config/node/null_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/config/node/script_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb73
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb148
-rw-r--r--spec/lib/gitlab/database_spec.rb16
-rw-r--r--spec/lib/gitlab/email/message/repository_push_spec.rb2
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb4
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb8
-rw-r--r--spec/lib/gitlab/github_import/comment_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/hook_formatter_spec.rb65
-rw-r--r--spec/lib/gitlab/gitignore_spec.rb40
-rw-r--r--spec/lib/gitlab/gitlab_import/client_spec.rb4
-rw-r--r--spec/lib/gitlab/import_url_spec.rb21
-rw-r--r--spec/lib/gitlab/lazy_spec.rb37
-rw-r--r--spec/lib/gitlab/lfs/lfs_router_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb68
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb29
-rw-r--r--spec/lib/gitlab/metrics/sampler_spec.rb27
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/rails_queue_duration_spec.rb31
-rw-r--r--spec/lib/gitlab/note_data_builder_spec.rb71
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb12
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb18
-rw-r--r--spec/lib/gitlab/sanitizers/svg_spec.rb94
-rw-r--r--spec/lib/gitlab/search_results_spec.rb16
-rw-r--r--spec/lib/gitlab/sherlock/collection_spec.rb6
-rw-r--r--spec/lib/gitlab/sherlock/query_spec.rb2
-rw-r--r--spec/lib/gitlab/sherlock/transaction_spec.rb4
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb68
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb2
-rw-r--r--spec/lib/json_web_token/rsa_token_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb286
-rw-r--r--spec/mailers/previews/devise_mailer_preview.rb11
-rw-r--r--spec/mailers/shared/notify.rb4
-rw-r--r--spec/models/ability_spec.rb117
-rw-r--r--spec/models/award_emoji_spec.rb30
-rw-r--r--spec/models/build_spec.rb196
-rw-r--r--spec/models/ci/commit_spec.rb404
-rw-r--r--spec/models/ci/pipeline_spec.rb403
-rw-r--r--spec/models/ci/runner_project_spec.rb5
-rw-r--r--spec/models/ci/runner_spec.rb32
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/commit_range_spec.rb34
-rw-r--r--spec/models/commit_spec.rb38
-rw-r--r--spec/models/commit_status_spec.rb50
-rw-r--r--spec/models/concerns/access_requestable_spec.rb40
-rw-r--r--spec/models/concerns/awardable_spec.rb48
-rw-r--r--spec/models/concerns/issuable_spec.rb84
-rw-r--r--spec/models/concerns/milestoneish_spec.rb14
-rw-r--r--spec/models/concerns/participable_spec.rb83
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb6
-rw-r--r--spec/models/event_spec.rb6
-rw-r--r--spec/models/generic_commit_status_spec.rb8
-rw-r--r--spec/models/group_spec.rb48
-rw-r--r--spec/models/issue_spec.rb57
-rw-r--r--spec/models/legacy_diff_note_spec.rb4
-rw-r--r--spec/models/member_spec.rb124
-rw-r--r--spec/models/members/group_member_spec.rb28
-rw-r--r--spec/models/members/project_member_spec.rb70
-rw-r--r--spec/models/merge_request_spec.rb222
-rw-r--r--spec/models/namespace_spec.rb14
-rw-r--r--spec/models/note_spec.rb126
-rw-r--r--spec/models/notification_setting_spec.rb1
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb14
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb151
-rw-r--r--spec/models/project_services/slack_service/build_message_spec.rb23
-rw-r--r--spec/models/project_services/slack_service/issue_message_spec.rb11
-rw-r--r--spec/models/project_services/slack_service_spec.rb68
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb12
-rw-r--r--spec/models/project_spec.rb215
-rw-r--r--spec/models/project_team_spec.rb144
-rw-r--r--spec/models/project_wiki_spec.rb13
-rw-r--r--spec/models/repository_spec.rb46
-rw-r--r--spec/models/service_spec.rb33
-rw-r--r--spec/models/snippet_spec.rb27
-rw-r--r--spec/models/user_spec.rb149
-rw-r--r--spec/requests/api/builds_spec.rb44
-rw-r--r--spec/requests/api/commit_statuses_spec.rb8
-rw-r--r--spec/requests/api/commits_spec.rb4
-rw-r--r--spec/requests/api/gitignores_spec.rb29
-rw-r--r--spec/requests/api/groups_spec.rb21
-rw-r--r--spec/requests/api/issues_spec.rb26
-rw-r--r--spec/requests/api/licenses_spec.rb12
-rw-r--r--spec/requests/api/merge_requests_spec.rb44
-rw-r--r--spec/requests/api/milestones_spec.rb13
-rw-r--r--spec/requests/api/notes_spec.rb71
-rw-r--r--spec/requests/api/project_members_spec.rb2
-rw-r--r--spec/requests/api/runners_spec.rb15
-rw-r--r--spec/requests/api/system_hooks_spec.rb2
-rw-r--r--spec/requests/api/triggers_spec.rb12
-rw-r--r--spec/requests/api/users_spec.rb2
-rw-r--r--spec/requests/ci/api/builds_spec.rb116
-rw-r--r--spec/requests/ci/api/runners_spec.rb83
-rw-r--r--spec/requests/ci/api/triggers_spec.rb12
-rw-r--r--spec/requests/git_http_spec.rb395
-rw-r--r--spec/requests/jwt_controller_spec.rb4
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb88
-rw-r--r--spec/services/ci/create_builds_service_spec.rb4
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb6
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb4
-rw-r--r--spec/services/ci/register_build_service_spec.rb4
-rw-r--r--spec/services/create_commit_builds_service_spec.rb162
-rw-r--r--spec/services/git_push_service_spec.rb43
-rw-r--r--spec/services/groups/create_service_spec.rb4
-rw-r--r--spec/services/issues/bulk_update_service_spec.rb286
-rw-r--r--spec/services/issues/create_service_spec.rb6
-rw-r--r--spec/services/issues/move_service_spec.rb11
-rw-r--r--spec/services/issues/update_service_spec.rb70
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb81
-rw-r--r--spec/services/merge_requests/create_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb15
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb48
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb28
-rw-r--r--spec/services/merge_requests/update_service_spec.rb8
-rw-r--r--spec/services/notes/create_service_spec.rb30
-rw-r--r--spec/services/notification_service_spec.rb342
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb12
-rw-r--r--spec/services/projects/destroy_service_spec.rb23
-rw-r--r--spec/services/projects/fork_service_spec.rb27
-rw-r--r--spec/services/projects/import_service_spec.rb4
-rw-r--r--spec/services/projects/transfer_service_spec.rb11
-rw-r--r--spec/services/system_note_service_spec.rb25
-rw-r--r--spec/services/todo_service_spec.rb100
-rw-r--r--spec/spec_helper.rb5
-rw-r--r--spec/support/fake_u2f_device.rb36
-rw-r--r--spec/support/filter_spec_helper.rb3
-rw-r--r--spec/support/import_spec_helper.rb (renamed from spec/controllers/import/import_spec_helper.rb)2
-rw-r--r--spec/support/login_helpers.rb6
-rw-r--r--spec/support/markdown_feature.rb10
-rw-r--r--spec/support/matchers/markdown_matchers.rb12
-rw-r--r--spec/support/reference_parser_helpers.rb5
-rw-r--r--spec/support/stub_gitlab_calls.rb25
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb32
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb62
-rw-r--r--spec/teaspoon_env.rb50
-rw-r--r--spec/workers/expire_build_artifacts_worker_spec.rb57
-rw-r--r--spec/workers/post_receive_spec.rb16
-rw-r--r--spec/workers/repository_import_worker_spec.rb26
-rw-r--r--spec/workers/stuck_ci_builds_worker_spec.rb19
298 files changed, 12631 insertions, 2300 deletions
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb
index 2ba0d489197..4cb8b8da150 100644
--- a/spec/controllers/admin/projects_controller_spec.rb
+++ b/spec/controllers/admin/projects_controller_spec.rb
@@ -17,7 +17,7 @@ describe Admin::ProjectsController do
it 'does not retrieve the project' do
get :index, visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]
- expect(response.body).to_not match(project.name)
+ expect(response.body).not_to match(project.name)
end
end
end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index a5986598715..89c2c26a367 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -4,17 +4,211 @@ describe Groups::GroupMembersController do
let(:user) { create(:user) }
let(:group) { create(:group) }
- context "index" do
+ describe '#index' do
before do
group.add_owner(user)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
it 'renders index with group members' do
- get :index, group_id: group.path
+ get :index, group_id: group
expect(response.status).to eq(200)
expect(response).to render_template(:index)
end
end
+
+ describe '#destroy' do
+ let(:group) { create(:group, :public) }
+
+ context 'when member is not found' do
+ it 'returns 403' do
+ delete :destroy, group_id: group,
+ id: 42
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:group_user) { create(:user) }
+ let(:member) do
+ group.add_developer(group_user)
+ group.members.find_by(user_id: group_user)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'returns 403' do
+ delete :destroy, group_id: group,
+ id: member
+
+ expect(response.status).to eq(403)
+ expect(group.users).to include group_user
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it '[HTML] removes user from members' do
+ delete :destroy, group_id: group,
+ id: member
+
+ expect(response).to set_flash.to 'User was successfully removed from group.'
+ expect(response).to redirect_to(group_group_members_path(group))
+ expect(group.users).not_to include group_user
+ end
+
+ it '[JS] removes user from members' do
+ xhr :delete, :destroy, group_id: group,
+ id: member
+
+ expect(response).to be_success
+ expect(group.users).not_to include group_user
+ end
+ end
+ end
+ end
+
+ describe '#leave' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ context 'when member is not found' do
+ before { sign_in(user) }
+
+ it 'returns 403' do
+ delete :leave, group_id: group
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when member is found' do
+ context 'and is not an owner' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, group_id: group
+
+ expect(response).to set_flash.to "You left the \"#{group.name}\" group."
+ expect(response).to redirect_to(dashboard_groups_path)
+ expect(group.users).not_to include user
+ end
+ end
+
+ context 'and is an owner' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'cannot removes himself from the group' do
+ delete :leave, group_id: group
+
+ expect(response).to redirect_to(group_path(group))
+ expect(response).to set_flash[:alert].to "You can not leave the \"#{group.name}\" group. Transfer or delete the group."
+ expect(group.users).to include user
+ end
+ end
+
+ context 'and is a requester' do
+ before do
+ group.request_access(user)
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, group_id: group
+
+ expect(response).to set_flash.to 'Your access request to the group has been withdrawn.'
+ expect(response).to redirect_to(dashboard_groups_path)
+ expect(group.members.request).to be_empty
+ expect(group.users).not_to include user
+ end
+ end
+ end
+ end
+
+ describe '#request_access' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'creates a new GroupMember that is not a team member' do
+ post :request_access, group_id: group
+
+ expect(response).to set_flash.to 'Your request for access has been queued for review.'
+ expect(response).to redirect_to(group_path(group))
+ expect(group.members.request.exists?(user_id: user)).to be_truthy
+ expect(group.users).not_to include user
+ end
+ end
+
+ describe '#approve_access_request' do
+ let(:group) { create(:group, :public) }
+
+ context 'when member is not found' do
+ it 'returns 403' do
+ post :approve_access_request, group_id: group,
+ id: 42
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:group_requester) { create(:user) }
+ let(:member) do
+ group.request_access(group_requester)
+ group.members.request.find_by(user_id: group_requester)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'returns 403' do
+ post :approve_access_request, group_id: group,
+ id: member
+
+ expect(response.status).to eq(403)
+ expect(group.users).not_to include group_requester
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'adds user to members' do
+ post :approve_access_request, group_id: group,
+ id: member
+
+ expect(response).to redirect_to(group_group_members_path(group))
+ expect(group.users).to include group_requester
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 465531b2b36..cd98fecd0c7 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -31,9 +31,9 @@ describe GroupsController do
let(:issue_2) { create(:issue, project: project) }
before do
- create_list(:upvote_note, 3, project: project, noteable: issue_2)
- create_list(:upvote_note, 2, project: project, noteable: issue_1)
- create_list(:downvote_note, 2, project: project, noteable: issue_2)
+ create_list(:award_emoji, 3, awardable: issue_2)
+ create_list(:award_emoji, 2, awardable: issue_1)
+ create_list(:award_emoji, 2, :downvote, awardable: issue_2,)
sign_in(user)
end
@@ -56,9 +56,9 @@ describe GroupsController do
let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
before do
- create_list(:upvote_note, 3, project: project, noteable: merge_request_2)
- create_list(:upvote_note, 2, project: project, noteable: merge_request_1)
- create_list(:downvote_note, 2, project: project, noteable: merge_request_2)
+ create_list(:award_emoji, 3, awardable: merge_request_2)
+ create_list(:award_emoji, 2, awardable: merge_request_1)
+ create_list(:award_emoji, 2, :downvote, awardable: merge_request_2)
sign_in(user)
end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 81c03c9059b..07bf8d2d1c3 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::BitbucketController do
include ImportSpecHelper
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
index 27b11267d2a..5f0f6dea821 100644
--- a/spec/controllers/import/fogbugz_controller_spec.rb
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::FogbugzController do
include ImportSpecHelper
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index bcc713dce2a..c55a3c28208 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::GithubController do
include ImportSpecHelper
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index 198d006af76..e8cf6aa7767 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::GitlabController do
include ImportSpecHelper
diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb
index 7cb1b85a46d..4ae2b78e11c 100644
--- a/spec/controllers/import/gitorious_controller_spec.rb
+++ b/spec/controllers/import/gitorious_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::GitoriousController do
include ImportSpecHelper
diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb
index 66088139a69..4241db6e771 100644
--- a/spec/controllers/import/google_code_controller_spec.rb
+++ b/spec/controllers/import/google_code_controller_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require_relative 'import_spec_helper'
describe Import::GoogleCodeController do
include ImportSpecHelper
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
new file mode 100644
index 00000000000..af378304893
--- /dev/null
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Oauth::ApplicationsController do
+ let(:user) { create(:user) }
+
+ context 'project members' do
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it 'shows list of applications' do
+ get :index
+
+ expect(response.status).to eq(200)
+ end
+
+ it 'redirects back to profile page if OAuth applications are disabled' do
+ settings = double(user_oauth_applications?: false)
+ allow_any_instance_of(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(settings)
+
+ get :index
+
+ expect(response.status).to eq(302)
+ expect(response).to redirect_to(profile_path)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index 4fb1473c2d2..d08d0018b35 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -8,21 +8,21 @@ describe Profiles::TwoFactorAuthsController do
allow(subject).to receive(:current_user).and_return(user)
end
- describe 'GET new' do
+ describe 'GET show' do
let(:user) { create(:user) }
it 'generates otp_secret for user' do
expect(User).to receive(:generate_otp_secret).with(32).and_return('secret').once
- get :new
- get :new # Second hit shouldn't re-generate it
+ get :show
+ get :show # Second hit shouldn't re-generate it
end
it 'assigns qr_code' do
code = double('qr code')
expect(subject).to receive(:build_qr_code).and_return(code)
- get :new
+ get :show
expect(assigns[:qr_code]).to eq code
end
end
@@ -40,7 +40,7 @@ describe Profiles::TwoFactorAuthsController do
expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true)
end
- it 'sets two_factor_enabled' do
+ it 'enables 2fa for the user' do
go
user.reload
@@ -79,9 +79,9 @@ describe Profiles::TwoFactorAuthsController do
expect(assigns[:qr_code]).to eq code
end
- it 'renders new' do
+ it 'renders show' do
go
- expect(response).to render_template(:new)
+ expect(response).to render_template(:show)
end
end
end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 8ad73472117..c4b4a888b4e 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -122,27 +122,23 @@ describe Projects::BranchesController do
let(:branch) { "feature" }
it { expect(response.status).to eq(200) }
- it { expect(subject).to render_template('destroy') }
end
context "valid branch name with unencoded slashes" do
let(:branch) { "improve/awesome" }
it { expect(response.status).to eq(200) }
- it { expect(subject).to render_template('destroy') }
end
context "valid branch name with encoded slashes" do
let(:branch) { "improve%2Fawesome" }
it { expect(response.status).to eq(200) }
- it { expect(subject).to render_template('destroy') }
end
context "invalid branch name, valid ref" do
let(:branch) { "no-branch" }
it { expect(response.status).to eq(404) }
- it { expect(subject).to render_template('destroy') }
end
end
end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 788a609ee40..4018dac95a2 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::CompareController do
to: ref_to)
expect(response).to be_success
- expect(assigns(:diffs).first).to_not be_nil
+ expect(assigns(:diffs).first).not_to be_nil
expect(assigns(:commits).length).to be >= 1
end
@@ -32,7 +32,7 @@ describe Projects::CompareController do
w: 1)
expect(response).to be_success
- expect(assigns(:diffs).first).to_not be_nil
+ expect(assigns(:diffs).first).not_to be_nil
expect(assigns(:commits).length).to be >= 1
# without whitespace option, there are more than 2 diff_splits
diff_splits = assigns(:diffs).first.diff.split("\n")
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index 40bd83af861..fbe8758dda7 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -28,7 +28,7 @@ describe Projects::GroupLinksController do
expect(group.shared_projects).to include project
end
- it 'redirects to project group links page'do
+ it 'redirects to project group links page' do
expect(response).to redirect_to(
namespace_project_group_links_path(project.namespace, project)
)
@@ -43,7 +43,7 @@ describe Projects::GroupLinksController do
end
it 'does not share project with that group' do
- expect(group.shared_projects).to_not include project
+ expect(group.shared_projects).not_to include project
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 2b2ad3b9412..cbaa3e0b7b2 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -56,7 +56,7 @@ describe Projects::IssuesController do
move_issue
expect(response).to have_http_status :found
- expect(another_project.issues).to_not be_empty
+ expect(another_project.issues).not_to be_empty
end
end
@@ -105,6 +105,15 @@ describe Projects::IssuesController do
expect(assigns(:issues)).to eq [issue]
end
+ it 'should not list confidential issues for project members with guest role' do
+ sign_in(member)
+ project.team << [member, :guest]
+
+ get_issues
+
+ expect(assigns(:issues)).to eq [issue]
+ end
+
it 'should list confidential issues for author' do
sign_in(author)
get_issues
@@ -148,7 +157,7 @@ describe Projects::IssuesController do
shared_examples_for 'restricted action' do |http_status|
it 'returns 404 for guests' do
- sign_out :user
+ sign_out(:user)
go(id: unescaped_parameter_value.to_param)
expect(response).to have_http_status :not_found
@@ -161,6 +170,14 @@ describe Projects::IssuesController do
expect(response).to have_http_status :not_found
end
+ it 'returns 404 for project members with guest role' do
+ sign_in(member)
+ project.team << [member, :guest]
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status :not_found
+ end
+
it "returns #{http_status[:success]} for author" do
sign_in(author)
go(id: unescaped_parameter_value.to_param)
@@ -250,4 +267,20 @@ describe Projects::IssuesController do
end
end
end
+
+ describe 'POST #toggle_award_emoji' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it "toggles the award emoji" do
+ expect do
+ post(:toggle_award_emoji, namespace_id: project.namespace.path,
+ project_id: project.path, id: issue.iid, name: "thumbsup")
+ end.to change { issue.award_emoji.count }.by(1)
+
+ expect(response.status).to eq(200)
+ end
+ end
end
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
new file mode 100644
index 00000000000..ab1dd34ed57
--- /dev/null
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe Projects::LabelsController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ def create_label(attributes)
+ create(:label, attributes.merge(project: project))
+ end
+
+ before do
+ 15.times { |i| create_label(priority: (i % 3) + 1, title: "label #{15 - i}") }
+ 5.times { |i| create_label(title: "label #{100 - i}") }
+
+
+ get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+ end
+
+ context '@prioritized_labels' do
+ let(:prioritized_labels) { assigns(:prioritized_labels) }
+
+ it 'contains only prioritized labels' do
+ expect(prioritized_labels).to all(have_attributes(priority: a_value > 0))
+ end
+
+ it 'is sorted by priority, then label title' do
+ priorities_and_titles = prioritized_labels.pluck(:priority, :title)
+
+ expect(priorities_and_titles.sort).to eq(priorities_and_titles)
+ end
+ end
+
+ context '@labels' do
+ let(:labels) { assigns(:labels) }
+
+ it 'contains only unprioritized labels' do
+ expect(labels).to all(have_attributes(priority: nil))
+ end
+
+ it 'is sorted by label title' do
+ titles = labels.pluck(:title)
+
+ expect(titles.sort).to eq(titles)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c0a1f45195f..4b408c03703 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do
id: merge_request.iid,
format: format)
- expect(response.body).to eq((merge_request.send(:"to_#{format}")).to_s)
+ expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s)
end
it "should not escape Html" do
@@ -84,17 +84,14 @@ describe Projects::MergeRequestsController do
end
describe "as diff" do
- include_examples "export merge as", :diff
- let(:format) { :diff }
-
- it "should really only be a git diff" do
+ it "triggers workhorse to serve the request" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: merge_request.iid,
- format: format)
+ format: :diff)
- expect(response.body).to start_with("diff --git")
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
end
end
@@ -185,6 +182,92 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'POST #merge' do
+ let(:base_params) do
+ {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: merge_request.iid,
+ format: 'raw'
+ }
+ end
+
+ context 'when the user does not have access' do
+ before do
+ project.team.truncate
+ project.team << [user, :reporter]
+ post :merge, base_params
+ end
+
+ it 'returns not found' do
+ expect(response).to be_not_found
+ end
+ end
+
+ context 'when the merge request is not mergeable' do
+ before do
+ merge_request.update_attributes(title: "WIP: #{merge_request.title}")
+
+ post :merge, base_params
+ end
+
+ it 'returns :failed' do
+ expect(assigns(:status)).to eq(:failed)
+ end
+ end
+
+ context 'when the sha parameter does not match the source SHA' do
+ before { post :merge, base_params.merge(sha: 'foo') }
+
+ it 'returns :sha_mismatch' do
+ expect(assigns(:status)).to eq(:sha_mismatch)
+ end
+ end
+
+ context 'when the sha parameter matches the source SHA' do
+ def merge_with_sha
+ post :merge, base_params.merge(sha: merge_request.source_sha)
+ end
+
+ it 'returns :success' do
+ merge_with_sha
+
+ expect(assigns(:status)).to eq(:success)
+ end
+
+ it 'starts the merge immediately' do
+ expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)
+
+ merge_with_sha
+ end
+
+ context 'when merge_when_build_succeeds is passed' do
+ def merge_when_build_succeeds
+ post :merge, base_params.merge(sha: merge_request.source_sha, merge_when_build_succeeds: '1')
+ end
+
+ before do
+ create(:ci_empty_pipeline, project: project, sha: merge_request.source_sha, ref: merge_request.source_branch)
+ end
+
+ it 'returns :merge_when_build_succeeds' do
+ merge_when_build_succeeds
+
+ expect(assigns(:status)).to eq(:merge_when_build_succeeds)
+ end
+
+ it 'sets the MR to merge when the build succeeds' do
+ service = double(:merge_when_build_succeeds_service)
+
+ expect(MergeRequests::MergeWhenBuildSucceedsService).to receive(:new).with(project, anything, anything).and_return(service)
+ expect(service).to receive(:execute).with(merge_request)
+
+ merge_when_build_succeeds
+ end
+ end
+ end
+ end
+
describe "DELETE #destroy" do
it "denies access to users unless they're admin or project owner" do
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
new file mode 100644
index 00000000000..00bc38b6071
--- /dev/null
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -0,0 +1,36 @@
+require('spec_helper')
+
+describe Projects::NotesController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:note) { create(:note, noteable: issue, project: project) }
+
+ describe 'POST #toggle_award_emoji' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it "toggles the award emoji" do
+ expect do
+ post(:toggle_award_emoji, namespace_id: project.namespace.path,
+ project_id: project.path, id: note.id, name: "thumbsup")
+ end.to change { note.award_emoji.count }.by(1)
+
+ expect(response.status).to eq(200)
+ end
+
+ it "removes the already awarded emoji" do
+ post(:toggle_award_emoji, namespace_id: project.namespace.path,
+ project_id: project.path, id: note.id, name: "thumbsup")
+
+ expect do
+ post(:toggle_award_emoji, namespace_id: project.namespace.path,
+ project_id: project.path, id: note.id, name: "thumbsup")
+ end.to change { AwardEmoji.count }.by(-1)
+
+ expect(response.status).to eq(200)
+ end
+ end
+end
diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb
index 4908b545648..c5d17d97ec9 100644
--- a/spec/controllers/projects/notification_settings_controller_spec.rb
+++ b/spec/controllers/projects/notification_settings_controller_spec.rb
@@ -34,5 +34,19 @@ describe Projects::NotificationSettingsController do
expect(response.status).to eq 200
end
end
+
+ context 'not authorized' do
+ let(:private_project) { create(:project, :private) }
+ before { sign_in(user) }
+
+ it 'returns 404' do
+ put :update,
+ namespace_id: private_project.namespace.to_param,
+ project_id: private_project.to_param,
+ notification_setting: { level: :participating }
+
+ expect(response.status).to eq(404)
+ end
+ end
end
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index ed64e7cf9af..fc5f458e795 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -1,22 +1,22 @@
require('spec_helper')
describe Projects::ProjectMembersController do
- let(:project) { create(:project) }
- let(:another_project) { create(:project, :private) }
- let(:user) { create(:user) }
- let(:member) { create(:user) }
-
- before do
- project.team << [user, :master]
- another_project.team << [member, :guest]
- sign_in(user)
- end
-
describe '#apply_import' do
+ let(:project) { create(:project) }
+ let(:another_project) { create(:project, :private) }
+ let(:user) { create(:user) }
+ let(:member) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ another_project.team << [member, :guest]
+ sign_in(user)
+ end
+
shared_context 'import applied' do
before do
- post(:apply_import, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ post(:apply_import, namespace_id: project.namespace,
+ project_id: project,
source_project_id: another_project.id)
end
end
@@ -38,7 +38,7 @@ describe Projects::ProjectMembersController do
include_context 'import applied'
it 'does not import team members' do
- expect(project.team_members).to_not include member
+ expect(project.team_members).not_to include member
end
it 'responds with not found' do
@@ -48,18 +48,231 @@ describe Projects::ProjectMembersController do
end
describe '#index' do
- let(:project) { create(:project, :private) }
-
context 'when user is member' do
- let(:member) { create(:user) }
-
before do
+ project = create(:project, :private)
+ member = create(:user)
project.team << [member, :guest]
sign_in(member)
- get :index, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ get :index, namespace_id: project.namespace, project_id: project
end
it { expect(response.status).to eq(200) }
end
end
+
+ describe '#destroy' do
+ let(:project) { create(:project, :public) }
+
+ context 'when member is not found' do
+ it 'returns 404' do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: 42
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:team_user) { create(:user) }
+ let(:member) do
+ project.team << [team_user, :developer]
+ project.members.find_by(user_id: team_user.id)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ it 'returns 404' do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response.status).to eq(404)
+ expect(project.users).to include team_user
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ it '[HTML] removes user from members' do
+ delete :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to redirect_to(
+ namespace_project_project_members_path(project.namespace, project)
+ )
+ expect(project.users).not_to include team_user
+ end
+
+ it '[JS] removes user from members' do
+ xhr :delete, :destroy, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to be_success
+ expect(project.users).not_to include team_user
+ end
+ end
+ end
+ end
+
+ describe '#leave' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ context 'when member is not found' do
+ before { sign_in(user) }
+
+ it 'returns 403' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when member is found' do
+ context 'and is not an owner' do
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to set_flash.to "You left the \"#{project.human_name}\" project."
+ expect(response).to redirect_to(dashboard_projects_path)
+ expect(project.users).not_to include user
+ end
+ end
+
+ context 'and is an owner' do
+ before do
+ project.update(namespace_id: user.namespace_id)
+ project.team << [user, :master, user]
+ sign_in(user)
+ end
+
+ it 'cannot remove himself from the project' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to redirect_to(
+ namespace_project_path(project.namespace, project)
+ )
+ expect(response).to set_flash[:alert].to "You can not leave the \"#{project.human_name}\" project. Transfer or delete the project."
+ expect(project.users).to include user
+ end
+ end
+
+ context 'and is a requester' do
+ before do
+ project.request_access(user)
+ sign_in(user)
+ end
+
+ it 'removes user from members' do
+ delete :leave, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to set_flash.to 'Your access request to the project has been withdrawn.'
+ expect(response).to redirect_to(dashboard_projects_path)
+ expect(project.members.request).to be_empty
+ expect(project.users).not_to include user
+ end
+ end
+ end
+ end
+
+ describe '#request_access' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'creates a new ProjectMember that is not a team member' do
+ post :request_access, namespace_id: project.namespace,
+ project_id: project
+
+ expect(response).to set_flash.to 'Your request for access has been queued for review.'
+ expect(response).to redirect_to(
+ namespace_project_path(project.namespace, project)
+ )
+ expect(project.members.request.exists?(user_id: user)).to be_truthy
+ expect(project.users).not_to include user
+ end
+ end
+
+ describe '#approve' do
+ let(:project) { create(:project, :public) }
+
+ context 'when member is not found' do
+ it 'returns 404' do
+ post :approve_access_request, namespace_id: project.namespace,
+ project_id: project,
+ id: 42
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when member is found' do
+ let(:user) { create(:user) }
+ let(:team_requester) { create(:user) }
+ let(:member) do
+ project.request_access(team_requester)
+ project.members.request.find_by(user_id: team_requester.id)
+ end
+
+ context 'when user does not have enough rights' do
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ it 'returns 404' do
+ post :approve_access_request, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response.status).to eq(404)
+ expect(project.users).not_to include team_requester
+ end
+ end
+
+ context 'when user has enough rights' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ it 'adds user to members' do
+ post :approve_access_request, namespace_id: project.namespace,
+ project_id: project,
+ id: member
+
+ expect(response).to redirect_to(
+ namespace_project_project_members_path(project.namespace, project)
+ )
+ expect(project.users).to include team_requester
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 1caa476d37d..33c35161da3 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -17,6 +17,7 @@ describe Projects::RawController do
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
expect(response.header['Content-Disposition']).
to eq("inline")
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
end
end
@@ -31,6 +32,7 @@ describe Projects::RawController do
expect(response.status).to eq(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-blob:")
end
end
@@ -42,7 +44,7 @@ describe Projects::RawController do
before do
public_project.lfs_objects << lfs_object
allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
- allow(controller).to receive(:send_file) { controller.render nothing: true }
+ allow(controller).to receive(:send_file) { controller.head :ok }
end
it 'serves the file' do
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 0ddbec9eac2..aad62cf20e3 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -20,10 +20,11 @@ describe Projects::RepositoriesController do
project.team << [user, :developer]
sign_in(user)
end
- it "uses Gitlab::Workhorse" do
- expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip")
+ it "uses Gitlab::Workhorse" do
get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:")
end
context "when the service raises an error" do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 069cd917e5a..fba545560c7 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -8,6 +8,40 @@ describe ProjectsController do
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
describe "GET show" do
+ context "user not project member" do
+ before { sign_in(user) }
+
+ context "user does not have access to project" do
+ let(:private_project) { create(:project, :private) }
+
+ it "does not initialize notification setting" do
+ get :show, namespace_id: private_project.namespace.path, id: private_project.path
+ expect(assigns(:notification_setting)).to be_nil
+ end
+ end
+
+ context "user has access to project" do
+ context "and does not have notification setting" do
+ it "initializes notification as disabled" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ expect(assigns(:notification_setting).level).to eq("global")
+ end
+ end
+
+ context "and has notification setting" do
+ before do
+ setting = user.notification_settings_for(public_project)
+ setting.level = :watch
+ setting.save
+ end
+
+ it "shows current notification setting" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ expect(assigns(:notification_setting).level).to eq("watch")
+ end
+ end
+ end
+ end
context "rendering default project view" do
render_views
@@ -81,6 +115,17 @@ describe ProjectsController do
expect(public_project_with_dot_atom).not_to be_valid
end
end
+
+ context 'when the project is pending deletions' do
+ it 'renders a 404 error' do
+ project = create(:project, pending_delete: true)
+ sign_in(user)
+
+ get :show, namespace_id: project.namespace.path, id: project.path
+
+ expect(response.status).to eq 404
+ end
+ end
end
describe "#update" do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index df70a589a89..209fa37d97d 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -11,17 +11,17 @@ describe RegistrationsController do
let(:user_params) { { user: { name: "new_user", username: "new_username", email: "new@user.com", password: "Any_password" } } }
context 'when sending email confirmation' do
- before { allow(current_application_settings).to receive(:send_user_confirmation_email).and_return(false) }
+ before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) }
it 'logs user in directly' do
post(:create, user_params)
expect(ActionMailer::Base.deliveries.last).to be_nil
- expect(subject.current_user).to_not be_nil
+ expect(subject.current_user).not_to be_nil
end
end
context 'when not sending email confirmation' do
- before { allow(current_application_settings).to receive(:send_user_confirmation_email).and_return(true) }
+ before { allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) }
it 'does not authenticate user and sends confirmation email' do
post(:create, user_params)
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 83cc8ec6d26..4e9bfb0c69b 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -12,7 +12,7 @@ describe SessionsController do
post(:create, user: { login: 'invalid', password: 'invalid' })
expect(response)
- .to set_flash.now[:alert].to /Invalid login or password/
+ .to set_flash.now[:alert].to /Invalid Login or password/
end
end
@@ -25,16 +25,42 @@ describe SessionsController do
expect(response).to set_flash.to /Signed in successfully/
expect(subject.current_user). to eq user
end
+
+ it "creates an audit log record" do
+ expect { post(:create, user: { login: user.username, password: user.password }) }.to change { SecurityEvent.count }.by(1)
+ expect(SecurityEvent.last.details[:with]).to eq("standard")
+ end
end
end
- context 'when using two-factor authentication' do
+ context 'when using two-factor authentication via OTP' do
let(:user) { create(:user, :two_factor) }
def authenticate_2fa(user_params)
post(:create, { user: user_params }, { otp_user_id: user.id })
end
+ context 'remember_me field' do
+ it 'sets a remember_user_token cookie when enabled' do
+ allow(controller).to receive(:find_user).and_return(user)
+ expect(controller).
+ to receive(:remember_me).with(user).and_call_original
+
+ authenticate_2fa(remember_me: '1', otp_attempt: user.current_otp)
+
+ expect(response.cookies['remember_user_token']).to be_present
+ end
+
+ it 'does nothing when disabled' do
+ allow(controller).to receive(:find_user).and_return(user)
+ expect(controller).not_to receive(:remember_me)
+
+ authenticate_2fa(remember_me: '0', otp_attempt: user.current_otp)
+
+ expect(response.cookies['remember_user_token']).to be_nil
+ end
+ end
+
##
# See #14900 issue
#
@@ -47,7 +73,7 @@ describe SessionsController do
authenticate_2fa(login: another_user.username,
otp_attempt: another_user.current_otp)
- expect(subject.current_user).to_not eq another_user
+ expect(subject.current_user).not_to eq another_user
end
end
@@ -56,7 +82,7 @@ describe SessionsController do
authenticate_2fa(login: another_user.username,
otp_attempt: 'invalid')
- expect(subject.current_user).to_not eq another_user
+ expect(subject.current_user).not_to eq another_user
end
end
@@ -73,7 +99,7 @@ describe SessionsController do
before { authenticate_2fa(otp_attempt: 'invalid') }
it 'does not authenticate' do
- expect(subject.current_user).to_not eq user
+ expect(subject.current_user).not_to eq user
end
it 'warns about invalid OTP code' do
@@ -96,6 +122,25 @@ describe SessionsController do
end
end
end
+
+ it "creates an audit log record" do
+ expect { authenticate_2fa(login: user.username, otp_attempt: user.current_otp) }.to change { SecurityEvent.count }.by(1)
+ expect(SecurityEvent.last.details[:with]).to eq("two-factor")
+ end
+ end
+
+ context 'when using two-factor authentication via U2F device' do
+ let(:user) { create(:user, :two_factor) }
+
+ def authenticate_2fa_u2f(user_params)
+ post(:create, { user: user_params }, { otp_user_id: user.id })
+ end
+
+ it "creates an audit log record" do
+ allow(U2fRegistration).to receive(:authenticate).and_return(true)
+ expect { authenticate_2fa_u2f(login: user.username, device_response: "{}") }.to change { SecurityEvent.count }.by(1)
+ expect(SecurityEvent.last.details[:with]).to eq("two-factor-via-u2f-device")
+ end
end
end
end
diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb
new file mode 100644
index 00000000000..4b858df52c9
--- /dev/null
+++ b/spec/factories/award_emoji.rb
@@ -0,0 +1,12 @@
+FactoryGirl.define do
+ factory :award_emoji do
+ name "thumbsup"
+ user
+ awardable factory: :issue
+
+ trait :upvote
+ trait :downvote do
+ name "thumbsdown"
+ end
+ end
+end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index cd49e559b7d..fe05a0cfc00 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -16,7 +16,7 @@ FactoryGirl.define do
}
end
- commit factory: :ci_commit
+ pipeline factory: :ci_pipeline
trait :success do
status 'success'
@@ -43,7 +43,7 @@ FactoryGirl.define do
end
after(:build) do |build, evaluator|
- build.project = build.commit.project
+ build.project = build.pipeline.project
end
factory :ci_not_started_build do
diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb
index 645cd7ae766..a039bef6f3c 100644
--- a/spec/factories/ci/commits.rb
+++ b/spec/factories/ci/commits.rb
@@ -17,30 +17,30 @@
#
FactoryGirl.define do
- factory :ci_empty_commit, class: Ci::Commit do
+ factory :ci_empty_pipeline, class: Ci::Pipeline do
sha '97de212e80737a608d939f648d959671fb0a0142'
project factory: :empty_project
- factory :ci_commit_without_jobs do
+ factory :ci_pipeline_without_jobs do
after(:build) do |commit|
allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) }
end
end
- factory :ci_commit_with_one_job do
+ factory :ci_pipeline_with_one_job do
after(:build) do |commit|
allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) }
end
end
- factory :ci_commit_with_two_jobs do
+ factory :ci_pipeline_with_two_job do
after(:build) do |commit|
allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) }
end
end
- factory :ci_commit do
+ factory :ci_pipeline do
after(:build) do |commit|
allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index b7c2b32cb13..1e5c479616c 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -3,12 +3,12 @@ FactoryGirl.define do
name 'default'
status 'success'
description 'commit status'
- commit factory: :ci_commit_with_one_job
+ pipeline factory: :ci_pipeline_with_one_job
started_at 'Tue, 26 Jan 2016 08:21:42 +0100'
finished_at 'Tue, 26 Jan 2016 08:23:42 +0100'
after(:build) do |build, evaluator|
- build.project = build.commit.project
+ build.project = build.pipeline.project
end
factory :generic_commit_status, class: GenericCommitStatus do
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 26719f2652c..696cf276e57 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -7,6 +7,7 @@ FactoryGirl.define do
project
note "Note"
author
+ on_issue
factory :note_on_commit, traits: [:on_commit]
factory :note_on_commit_diff, traits: [:on_commit, :on_diff], class: LegacyDiffNote
@@ -15,43 +16,34 @@ FactoryGirl.define do
factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff], class: LegacyDiffNote
factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :system_note, traits: [:system]
- factory :downvote_note, traits: [:award, :downvote]
- factory :upvote_note, traits: [:award, :upvote]
trait :on_commit do
- project
+ noteable nil
+ noteable_id nil
+ noteable_type 'Commit'
commit_id RepoHelpers.sample_commit.id
- noteable_type "Commit"
end
trait :on_diff do
line_code "0_184_184"
end
- trait :on_merge_request do
- project
- noteable_id 1
- noteable_type "MergeRequest"
+ trait :on_issue do
+ noteable { create(:issue, project: project) }
end
- trait :on_issue do
- noteable_id 1
- noteable_type "Issue"
+ trait :on_merge_request do
+ noteable { create(:merge_request, source_project: project) }
end
trait :on_project_snippet do
- noteable_id 1
- noteable_type "Snippet"
+ noteable { create(:snippet, project: project) }
end
trait :system do
system true
end
- trait :award do
- is_award true
- end
-
trait :downvote do
note "thumbsdown"
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index da8d97c9f82..5c8ddbebf0d 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -67,9 +67,6 @@ FactoryGirl.define do
'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new'
}
)
-
- project.issues_tracker = 'redmine'
- project.issues_tracker_id = 'project_name_in_redmine'
end
end
@@ -84,9 +81,6 @@ FactoryGirl.define do
'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa'
}
)
-
- project.issues_tracker = 'jira'
- project.issues_tracker_id = 'project_name_in_jira'
end
end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index e3681ae93a5..f426e27afed 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -18,5 +18,9 @@ FactoryGirl.define do
commit_id RepoHelpers.sample_commit.id
target_type "Commit"
end
+
+ trait :build_failed do
+ action { Todo::BUILD_FAILED }
+ end
end
end
diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb
new file mode 100644
index 00000000000..df92b079581
--- /dev/null
+++ b/spec/factories/u2f_registrations.rb
@@ -0,0 +1,8 @@
+FactoryGirl.define do
+ factory :u2f_registration do
+ certificate { FFaker::BaconIpsum.characters(728) }
+ key_handle { FFaker::BaconIpsum.characters(86) }
+ public_key { FFaker::BaconIpsum.characters(88) }
+ counter 0
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index a9b2148bd2a..c6f7869516e 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -15,14 +15,26 @@ FactoryGirl.define do
end
trait :two_factor do
+ two_factor_via_otp
+ end
+
+ trait :two_factor_via_otp do
before(:create) do |user|
- user.two_factor_enabled = true
+ user.otp_required_for_login = true
user.otp_secret = User.generate_otp_secret(32)
user.otp_grace_period_started_at = Time.now
user.generate_otp_backup_codes!
end
end
+ trait :two_factor_via_u2f do
+ transient { registrations_count 5 }
+
+ after(:create) do |user, evaluator|
+ create_list(:u2f_registration, evaluator.registrations_count, user: user)
+ end
+ end
+
factory :omniauth_user do
transient do
extern_uid '123456'
diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb
index 938ccf2306b..efa6cbe5bb1 100644
--- a/spec/factories/wiki_pages.rb
+++ b/spec/factories/wiki_pages.rb
@@ -2,7 +2,7 @@ require 'ostruct'
FactoryGirl.define do
factory :wiki_page do
- page = OpenStruct.new(url_path: 'some-name')
+ page { OpenStruct.new(url_path: 'some-name') }
association :wiki, factory: :project_wiki, strategy: :build
initialize_with { new(wiki, page, true) }
end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 62de081661d..675d9bd18b7 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -5,8 +5,8 @@ describe 'factories' do
describe "#{factory.name} factory" do
let(:entity) { build(factory.name) }
- it 'does not raise error when created 'do
- expect { entity }.to_not raise_error
+ it 'does not raise error when created' do
+ expect { entity }.not_to raise_error
end
it 'should be valid', if: factory.build_class < ActiveRecord::Base do
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index 7bbe20fec43..a6198389f04 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -6,15 +6,15 @@ describe 'Admin Builds' do
end
describe 'GET /admin/builds' do
- let(:commit) { create(:ci_commit) }
+ let(:pipeline) { create(:ci_pipeline) }
context 'All tab' do
context 'when have builds' do
it 'shows all builds' do
- create(:ci_build, commit: commit, status: :pending)
- create(:ci_build, commit: commit, status: :running)
- create(:ci_build, commit: commit, status: :success)
- create(:ci_build, commit: commit, status: :failed)
+ create(:ci_build, pipeline: pipeline, status: :pending)
+ create(:ci_build, pipeline: pipeline, status: :running)
+ create(:ci_build, pipeline: pipeline, status: :success)
+ create(:ci_build, pipeline: pipeline, status: :failed)
visit admin_builds_path
@@ -39,9 +39,9 @@ describe 'Admin Builds' do
context 'Running tab' do
context 'when have running builds' do
it 'shows running builds' do
- build1 = create(:ci_build, commit: commit, status: :pending)
- build2 = create(:ci_build, commit: commit, status: :success)
- build3 = create(:ci_build, commit: commit, status: :failed)
+ build1 = create(:ci_build, pipeline: pipeline, status: :pending)
+ build2 = create(:ci_build, pipeline: pipeline, status: :success)
+ build3 = create(:ci_build, pipeline: pipeline, status: :failed)
visit admin_builds_path(scope: :running)
@@ -55,7 +55,7 @@ describe 'Admin Builds' do
context 'when have no builds running' do
it 'shows a message' do
- create(:ci_build, commit: commit, status: :success)
+ create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :running)
@@ -69,9 +69,9 @@ describe 'Admin Builds' do
context 'Finished tab' do
context 'when have finished builds' do
it 'shows finished builds' do
- build1 = create(:ci_build, commit: commit, status: :pending)
- build2 = create(:ci_build, commit: commit, status: :running)
- build3 = create(:ci_build, commit: commit, status: :success)
+ build1 = create(:ci_build, pipeline: pipeline, status: :pending)
+ build2 = create(:ci_build, pipeline: pipeline, status: :running)
+ build3 = create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :finished)
@@ -85,7 +85,7 @@ describe 'Admin Builds' do
context 'when have no builds finished' do
it 'shows a message' do
- create(:ci_build, commit: commit, status: :running)
+ create(:ci_build, pipeline: pipeline, status: :running)
visit admin_builds_path(scope: :finished)
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 26d03944b8a..9499cd4e025 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -8,8 +8,8 @@ describe "Admin Runners" do
describe "Runners page" do
before do
runner = FactoryGirl.create(:ci_runner)
- commit = FactoryGirl.create(:ci_commit)
- FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id)
+ pipeline = FactoryGirl.create(:ci_pipeline)
+ FactoryGirl.create(:ci_build, pipeline: pipeline, runner_id: runner.id)
visit admin_runners_path
end
@@ -79,7 +79,7 @@ describe "Admin Runners" do
end
it 'changes registration token' do
- expect(page_token).to_not eq token
+ expect(page_token).not_to eq token
end
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 6dee0cd8d47..1cb709c1de3 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -19,7 +19,7 @@ describe "Admin::Users", feature: true do
describe 'Two-factor Authentication filters' do
it 'counts users who have enabled 2FA' do
- create(:user, two_factor_enabled: true)
+ create(:user, :two_factor)
visit admin_users_path
@@ -29,7 +29,7 @@ describe "Admin::Users", feature: true do
end
it 'filters by users who have enabled 2FA' do
- user = create(:user, two_factor_enabled: true)
+ user = create(:user, :two_factor)
visit admin_users_path
click_link '2FA Enabled'
@@ -38,7 +38,7 @@ describe "Admin::Users", feature: true do
end
it 'counts users who have not enabled 2FA' do
- create(:user, two_factor_enabled: false)
+ create(:user)
visit admin_users_path
@@ -48,7 +48,7 @@ describe "Admin::Users", feature: true do
end
it 'filters by users who have not enabled 2FA' do
- user = create(:user, two_factor_enabled: false)
+ user = create(:user)
visit admin_users_path
click_link '2FA Disabled'
@@ -144,22 +144,22 @@ describe "Admin::Users", feature: true do
before { click_link 'Impersonate' }
it 'logs in as the user when impersonate is clicked' do
- page.within '.sidebar-user .username' do
- expect(page).to have_content(another_user.username)
+ page.within '.sidebar-wrapper' do
+ expect(page.find('.sidebar-user')['data-user']).to eql(another_user.username)
end
end
it 'sees impersonation log out icon' do
icon = first('.fa.fa-user-secret')
- expect(icon).to_not eql nil
+ expect(icon).not_to eql nil
end
it 'can log out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click
- page.within '.sidebar-user .username' do
- expect(page).to have_content(@user.username)
+ page.within '.sidebar-wrapper' do
+ expect(page.find('.sidebar-user')['data-user']).to eql(@user.username)
end
end
@@ -173,7 +173,7 @@ describe "Admin::Users", feature: true do
describe 'Two-factor Authentication status' do
it 'shows when enabled' do
- @user.update_attribute(:two_factor_enabled, true)
+ @user.update_attribute(:otp_required_for_login, true)
visit admin_user_path(@user)
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index b710cb3c72f..4dd9548cfc5 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -5,8 +5,6 @@ describe "Dashboard Issues Feed", feature: true do
let!(:user) { create(:user) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project) }
- let!(:issue1) { create(:issue, author: user, assignee: user, project: project1) }
- let!(:issue2) { create(:issue, author: user, assignee: user, project: project2) }
before do
project1.team << [user, :master]
@@ -14,16 +12,51 @@ describe "Dashboard Issues Feed", feature: true do
end
describe "atom feed" do
- it "should render atom feed via private token" do
+ it "renders atom feed via private token" do
visit issues_dashboard_path(:atom, private_token: user.private_token)
- expect(response_headers['Content-Type']).
- to have_content('application/atom+xml')
+ expect(response_headers['Content-Type']).to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{user.name} issues")
- expect(body).to have_selector('author email', text: issue1.author_email)
- expect(body).to have_selector('entry summary', text: issue1.title)
- expect(body).to have_selector('author email', text: issue2.author_email)
- expect(body).to have_selector('entry summary', text: issue2.title)
+ end
+
+ context "issue with basic fields" do
+ let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') }
+
+ it "renders issue fields" do
+ visit issues_dashboard_path(:atom, private_token: user.private_token)
+
+ entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
+
+ expect(entry).to be_present
+ expect(entry).to have_selector('author email', text: issue2.author_email)
+ expect(entry).to have_selector('assignee email', text: issue2.author_email)
+ expect(entry).not_to have_selector('labels')
+ expect(entry).not_to have_selector('milestone')
+ expect(entry).to have_selector('description', text: issue2.description)
+ end
+ end
+
+ context "issue with label and milestone" do
+ let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
+ let!(:label1) { create(:label, project: project1, title: 'label1') }
+ let!(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone1) }
+
+ before do
+ issue1.labels << label1
+ end
+
+ it "renders issue label and milestone info" do
+ visit issues_dashboard_path(:atom, private_token: user.private_token)
+
+ entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
+
+ expect(entry).to be_present
+ expect(entry).to have_selector('author email', text: issue1.author_email)
+ expect(entry).to have_selector('assignee email', text: issue1.author_email)
+ expect(entry).to have_selector('labels label', text: label1.title)
+ expect(entry).to have_selector('milestone', text: milestone1.title)
+ expect(entry).not_to have_selector('description')
+ end
end
end
end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index f83a78308e3..16832c297ac 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -5,8 +5,9 @@ describe "Builds" do
before do
login_as(:user)
- @commit = FactoryGirl.create :ci_commit
- @build = FactoryGirl.create :ci_build, commit: @commit
+ @commit = FactoryGirl.create :ci_pipeline
+ @build = FactoryGirl.create :ci_build, pipeline: @commit
+ @build2 = FactoryGirl.create :ci_build
@project = @commit.project
@project.team << [@user, :developer]
end
@@ -43,11 +44,10 @@ describe "Builds" do
end
it { expect(page).to have_selector('.nav-links li.active', text: 'All') }
- it { expect(page).to have_selector('.row-content-block', text: 'All builds from this project') }
it { expect(page).to have_content @build.short_sha }
it { expect(page).to have_content @build.ref }
it { expect(page).to have_content @build.name }
- it { expect(page).to_not have_link 'Cancel running' }
+ it { expect(page).not_to have_link 'Cancel running' }
end
end
@@ -63,17 +63,28 @@ describe "Builds" do
it { expect(page).to have_content @build.short_sha }
it { expect(page).to have_content @build.ref }
it { expect(page).to have_content @build.name }
- it { expect(page).to_not have_link 'Cancel running' }
+ it { expect(page).not_to have_link 'Cancel running' }
end
describe "GET /:project/builds/:id" do
- before do
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ context "Build from project" do
+ before do
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it { expect(page.status_code).to eq(200) }
+ it { expect(page).to have_content @commit.sha[0..7] }
+ it { expect(page).to have_content @commit.git_commit_message }
+ it { expect(page).to have_content @commit.git_author_name }
end
- it { expect(page).to have_content @commit.sha[0..7] }
- it { expect(page).to have_content @commit.git_commit_message }
- it { expect(page).to have_content @commit.git_author_name }
+ context "Build from other project" do
+ before do
+ visit namespace_project_build_path(@project.namespace, @project, @build2)
+ end
+
+ it { expect(page.status_code).to eq(404) }
+ end
context "Download artifacts" do
before do
@@ -82,8 +93,42 @@ describe "Builds" do
end
it 'has button to download artifacts' do
- page.within('.artifacts') do
- expect(page).to have_content 'Download'
+ expect(page).to have_content 'Download'
+ end
+ end
+
+ context 'Artifacts expire date' do
+ before do
+ @build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at)
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ context 'no expire date defined' do
+ let(:expire_at) { nil }
+
+ it 'does not have the Keep button' do
+ expect(page).not_to have_content 'Keep'
+ end
+ end
+
+ context 'when expire date is defined' do
+ let(:expire_at) { Time.now + 7.days }
+
+ it 'keeps artifacts when Keep button is clicked' do
+ expect(page).to have_content 'The artifacts will be removed'
+ click_link 'Keep'
+
+ expect(page).not_to have_link 'Keep'
+ expect(page).not_to have_content 'The artifacts will be removed'
+ end
+ end
+
+ context 'when artifacts expired' do
+ let(:expire_at) { Time.now - 7.days }
+
+ it 'does not have the Keep button' do
+ expect(page).to have_content 'The artifacts were removed'
+ expect(page).not_to have_link 'Keep'
end
end
end
@@ -96,59 +141,144 @@ describe "Builds" do
end
it do
- page.within('.build-controls') do
- expect(page).to have_link 'Raw'
- end
+ expect(page).to have_link 'Raw'
end
end
end
describe "POST /:project/builds/:id/cancel" do
- before do
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
- click_link "Cancel"
+ context "Build from project" do
+ before do
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ click_link "Cancel"
+ end
+
+ it { expect(page.status_code).to eq(200) }
+ it { expect(page).to have_content 'canceled' }
+ it { expect(page).to have_content 'Retry' }
end
- it { expect(page).to have_content 'canceled' }
- it { expect(page).to have_content 'Retry' }
+ context "Build from other project" do
+ before do
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ page.driver.post(cancel_namespace_project_build_path(@project.namespace, @project, @build2))
+ end
+
+ it { expect(page.status_code).to eq(404) }
+ end
end
describe "POST /:project/builds/:id/retry" do
- before do
- @build.run!
- visit namespace_project_build_path(@project.namespace, @project, @build)
- click_link "Cancel"
- click_link 'Retry'
+ context "Build from project" do
+ before do
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ click_link 'Cancel'
+ click_link 'Retry'
+ end
+
+ it { expect(page.status_code).to eq(200) }
+ it { expect(page).to have_content 'pending' }
+ it { expect(page).to have_content 'Cancel' }
end
- it { expect(page).to have_content 'pending' }
- it { expect(page).to have_content 'Cancel' }
+ context "Build from other project" do
+ before do
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ click_link 'Cancel'
+ page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2))
+ end
+
+ it { expect(page.status_code).to eq(404) }
+ end
end
describe "GET /:project/builds/:id/download" do
before do
@build.update_attributes(artifacts_file: artifacts_file)
visit namespace_project_build_path(@project.namespace, @project, @build)
- page.within('.artifacts') { click_link 'Download' }
+ click_link 'Download'
end
- it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
+ context "Build from other project" do
+ before do
+ @build2.update_attributes(artifacts_file: artifacts_file)
+ visit download_namespace_project_build_artifacts_path(@project.namespace, @project, @build2)
+ end
+
+ it { expect(page.status_code).to eq(404) }
+ end
end
describe "GET /:project/builds/:id/raw" do
- before do
- Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build.run!
- @build.trace = 'BUILD TRACE'
- visit namespace_project_build_path(@project.namespace, @project, @build)
+ context "Build from project" do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ @build.trace = 'BUILD TRACE'
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ end
+ end
+
+ context "Build from other project" do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build2.run!
+ @build2.trace = 'BUILD TRACE'
+ visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
+ puts page.status_code
+ puts current_url
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(404)
+ end
+ end
+ end
+
+ describe "GET /:project/builds/:id/trace.json" do
+ context "Build from project" do
+ before do
+ visit trace_namespace_project_build_path(@project.namespace, @project, @build, format: :json)
+ end
+
+ it { expect(page.status_code).to eq(200) }
+ end
+
+ context "Build from other project" do
+ before do
+ visit trace_namespace_project_build_path(@project.namespace, @project, @build2, format: :json)
+ end
+
+ it { expect(page.status_code).to eq(404) }
+ end
+ end
+
+ describe "GET /:project/builds/:id/status" do
+ context "Build from project" do
+ before do
+ visit status_namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it { expect(page.status_code).to eq(200) }
end
- it 'sends the right headers' do
- page.within('.build-controls') { click_link 'Raw' }
+ context "Build from other project" do
+ before do
+ visit status_namespace_project_build_path(@project.namespace, @project, @build2)
+ end
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ it { expect(page.status_code).to eq(404) }
end
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index dacaa96d760..45e1a157a1f 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -8,15 +8,15 @@ describe 'Commits' do
describe 'CI' do
before do
login_as :user
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
- let!(:commit) do
- FactoryGirl.create :ci_commit, project: project, sha: project.commit.sha
+ let!(:pipeline) do
+ FactoryGirl.create :ci_pipeline, project: project, sha: project.commit.sha
end
context 'commit status is Generic Commit Status' do
- let!(:status) { FactoryGirl.create :generic_commit_status, commit: commit }
+ let!(:status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline }
before do
project.team << [@user, :reporter]
@@ -24,10 +24,10 @@ describe 'Commits' do
describe 'Commit builds' do
before do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
end
- it { expect(page).to have_content commit.sha[0..7] }
+ it { expect(page).to have_content pipeline.sha[0..7] }
it 'contains generic commit status build' do
page.within('.table-holder') do
@@ -39,7 +39,7 @@ describe 'Commits' do
end
context 'commit status is Ci Build' do
- let!(:build) { FactoryGirl.create :ci_build, commit: commit }
+ let!(:build) { FactoryGirl.create :ci_build, pipeline: pipeline }
let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
context 'when logged as developer' do
@@ -53,7 +53,7 @@ describe 'Commits' do
end
it 'should show build status' do
- page.within("//li[@id='commit-#{commit.short_sha}']") do
+ page.within("//li[@id='commit-#{pipeline.short_sha}']") do
expect(page).to have_css(".ci-status-link")
end
end
@@ -61,12 +61,12 @@ describe 'Commits' do
describe 'Commit builds' do
before do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
end
- it { expect(page).to have_content commit.sha[0..7] }
- it { expect(page).to have_content commit.git_commit_message }
- it { expect(page).to have_content commit.git_author_name }
+ it { expect(page).to have_content pipeline.sha[0..7] }
+ it { expect(page).to have_content pipeline.git_commit_message }
+ it { expect(page).to have_content pipeline.git_author_name }
end
context 'Download artifacts' do
@@ -75,7 +75,7 @@ describe 'Commits' do
end
it do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
click_on 'Download artifacts'
expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type)
end
@@ -83,7 +83,7 @@ describe 'Commits' do
describe 'Cancel all builds' do
it 'cancels commit' do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
click_on 'Cancel running'
expect(page).to have_content 'canceled'
end
@@ -91,7 +91,7 @@ describe 'Commits' do
describe 'Cancel build' do
it 'cancels build' do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
click_on 'Cancel'
expect(page).to have_content 'canceled'
end
@@ -100,13 +100,13 @@ describe 'Commits' do
describe '.gitlab-ci.yml not found warning' do
context 'ci builds enabled' do
it "does not show warning" do
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
end
it 'shows warning' do
- stub_ci_commit_yaml_file(nil)
- visit ci_status_path(commit)
+ stub_ci_pipeline_yaml_file(nil)
+ visit ci_status_path(pipeline)
expect(page).to have_content '.gitlab-ci.yml not found in this commit'
end
end
@@ -114,8 +114,8 @@ describe 'Commits' do
context 'ci builds disabled' do
before do
stub_ci_builds_disabled
- stub_ci_commit_yaml_file(nil)
- visit ci_status_path(commit)
+ stub_ci_pipeline_yaml_file(nil)
+ visit ci_status_path(pipeline)
end
it 'does not show warning' do
@@ -129,16 +129,16 @@ describe 'Commits' do
before do
project.team << [@user, :reporter]
build.update_attributes(artifacts_file: artifacts_file)
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
end
it do
- expect(page).to have_content commit.sha[0..7]
- expect(page).to have_content commit.git_commit_message
- expect(page).to have_content commit.git_author_name
+ expect(page).to have_content pipeline.sha[0..7]
+ expect(page).to have_content pipeline.git_commit_message
+ expect(page).to have_content pipeline.git_author_name
expect(page).to have_link('Download artifacts')
- expect(page).to_not have_link('Cancel running')
- expect(page).to_not have_link('Retry failed')
+ expect(page).not_to have_link('Cancel running')
+ expect(page).not_to have_link('Retry failed')
end
end
@@ -148,16 +148,16 @@ describe 'Commits' do
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
public_builds: false)
build.update_attributes(artifacts_file: artifacts_file)
- visit ci_status_path(commit)
+ visit ci_status_path(pipeline)
end
it do
- expect(page).to have_content commit.sha[0..7]
- expect(page).to have_content commit.git_commit_message
- expect(page).to have_content commit.git_author_name
- expect(page).to_not have_link('Download artifacts')
- expect(page).to_not have_link('Cancel running')
- expect(page).to_not have_link('Retry failed')
+ expect(page).to have_content pipeline.sha[0..7]
+ expect(page).to have_content pipeline.git_commit_message
+ expect(page).to have_content pipeline.git_author_name
+ expect(page).not_to have_link('Download artifacts')
+ expect(page).not_to have_link('Cancel running')
+ expect(page).not_to have_link('Retry failed')
end
end
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
new file mode 100644
index 00000000000..53b4f027117
--- /dev/null
+++ b/spec/features/container_registry_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe "Container Registry" do
+ let(:project) { create(:empty_project) }
+ let(:repository) { project.container_registry_repository }
+ let(:tag_name) { 'latest' }
+ let(:tags) { [tag_name] }
+
+ before do
+ login_as(:user)
+ project.team << [@user, :developer]
+ stub_container_registry_tags(*tags)
+ stub_container_registry_config(enabled: true)
+ allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
+ end
+
+ describe 'GET /:project/container_registry' do
+ before do
+ visit namespace_project_container_registry_index_path(project.namespace, project)
+ end
+
+ context 'when no tags' do
+ let(:tags) { [] }
+
+ it { expect(page).to have_content('No images in Container Registry for this project') }
+ end
+
+ context 'when there are tags' do
+ it { expect(page).to have_content(tag_name)}
+ end
+ end
+
+ describe 'DELETE /:project/container_registry/tag' do
+ before do
+ visit namespace_project_container_registry_index_path(project.namespace, project)
+ end
+
+ it do
+ expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true)
+
+ click_on 'Remove'
+ end
+ end
+end
diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb
new file mode 100644
index 00000000000..365cb445df1
--- /dev/null
+++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+feature 'Tooltips on .timeago dates', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+ let(:created_date) { Date.yesterday.to_time }
+ let(:expected_format) { created_date.strftime('%b %-d, %Y %l:%M%P UTC') }
+
+ context 'on the activity tab' do
+ before do
+ project.team << [user, :master]
+
+ Event.create( project: project, author_id: user.id, action: Event::JOINED,
+ updated_at: created_date, created_at: created_date)
+
+ login_as user
+ visit user_path(user)
+ wait_for_ajax()
+
+ page.find('.js-timeago').hover
+ end
+
+ it 'has the datetime formated correctly' do
+ expect(page).to have_selector('.local-timeago', text: expected_format)
+ end
+ end
+
+ context 'on the snippets tab' do
+ before do
+ project.team << [user, :master]
+ create(:snippet, author: user, updated_at: created_date, created_at: created_date)
+
+ login_as user
+ visit user_snippets_path(user)
+ wait_for_ajax()
+
+ page.find('.js-timeago').hover
+ end
+
+ it 'has the datetime formated correctly' do
+ expect(page).to have_selector('.local-timeago', text: expected_format)
+ end
+ end
+end
diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb
new file mode 100644
index 00000000000..22525ce530b
--- /dev/null
+++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Owner manages access requests', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+
+ background do
+ group.request_access(user)
+ group.add_owner(owner)
+ login_as(owner)
+ end
+
+ scenario 'owner can see access requests' do
+ visit group_group_members_path(group)
+
+ expect_visible_access_request(group, user)
+ end
+
+ scenario 'master can grant access' do
+ visit group_group_members_path(group)
+
+ expect_visible_access_request(group, user)
+
+ perform_enqueued_jobs { click_on 'Grant access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was granted"
+ end
+
+ scenario 'master can deny access' do
+ visit group_group_members_path(group)
+
+ expect_visible_access_request(group, user)
+
+ perform_enqueued_jobs { click_on 'Deny access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{group.name} group was denied"
+ end
+
+
+ def expect_visible_access_request(group, user)
+ expect(group.members.request.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content "#{group.name} access requests (1)"
+ expect(page).to have_content user.name
+ end
+end
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
new file mode 100644
index 00000000000..a878a96b6ee
--- /dev/null
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+feature 'Groups > Members > User requests access', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+
+ background do
+ group.add_owner(owner)
+ login_as(user)
+ visit group_path(group)
+ end
+
+ scenario 'user can request access to a group' do
+ perform_enqueued_jobs { click_link 'Request Access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [owner.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Request to join the #{group.name} group"
+
+ expect(group.members.request.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content 'Your request for access has been queued for review.'
+
+ expect(page).to have_content 'Withdraw Access Request'
+ end
+
+ scenario 'user is not listed in the group members page' do
+ click_link 'Request Access'
+
+ expect(group.members.request.exists?(user_id: user)).to be_truthy
+
+ click_link 'Members'
+
+ page.within('.content') do
+ expect(page).not_to have_content(user.name)
+ end
+ end
+
+ scenario 'user can withdraw its request for access' do
+ click_link 'Request Access'
+
+ expect(group.members.request.exists?(user_id: user)).to be_truthy
+
+ click_link 'Withdraw Access Request'
+
+ expect(group.members.request.exists?(user_id: user)).to be_falsey
+ expect(page).to have_content 'Your access request to the group has been withdrawn.'
+ end
+end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 41af789aae2..07a854ea014 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -28,7 +28,6 @@ describe 'Awards Emoji', feature: true do
end
context 'click the thumbsup emoji' do
-
it 'should increment the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click
sleep 2
@@ -41,7 +40,6 @@ describe 'Awards Emoji', feature: true do
end
context 'click the thumbsdown emoji' do
-
it 'should increment the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
sleep 2
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
new file mode 100644
index 00000000000..63efecf8780
--- /dev/null
+++ b/spec/features/issues/award_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+feature 'Issue awards', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ describe 'logged in' do
+ before do
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should add award to issue' do
+ first('.js-emoji-btn').click
+ expect(page).to have_selector('.js-emoji-btn.active')
+ expect(first('.js-emoji-btn')).to have_content '1'
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ expect(first('.js-emoji-btn')).to have_content '1'
+ end
+
+ it 'should remove award from issue' do
+ first('.js-emoji-btn').click
+ find('.js-emoji-btn.active').click
+ expect(first('.js-emoji-btn')).to have_content '0'
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ expect(first('.js-emoji-btn')).to have_content '0'
+ end
+
+ it 'should only have one menu on the page' do
+ first('.js-add-award').click
+ expect(page).to have_selector('.emoji-menu')
+
+ expect(page).to have_selector('.emoji-menu', count: 1)
+ end
+ end
+
+ describe 'logged out' do
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should not see award menu button' do
+ expect(page).not_to have_selector('.js-award-holder')
+ end
+ end
+end
diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assigment_labels_spec.rb
new file mode 100644
index 00000000000..0fbc2062e39
--- /dev/null
+++ b/spec/features/issues/bulk_assigment_labels_spec.rb
@@ -0,0 +1,213 @@
+require 'rails_helper'
+
+feature 'Issues > Labels bulk assignment', feature: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let!(:project) { create(:project) }
+ let!(:issue1) { create(:issue, project: project, title: "Issue 1") }
+ let!(:issue2) { create(:issue, project: project, title: "Issue 2") }
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:feature) { create(:label, project: project, title: 'feature') }
+
+ context 'as a allowed user', js: true do
+ before do
+ project.team << [user, :master]
+
+ login_as user
+ end
+
+ context 'can bulk assign' do
+ before do
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ context 'a label' do
+ context 'to all issues' do
+ before do
+ check 'check_all_issues'
+ open_labels_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'bug'
+ end
+ end
+
+ context 'to a issue' do
+ before do
+ check "selected_issue_#{issue1.id}"
+ open_labels_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ end
+ end
+ end
+
+ context 'multiple labels' do
+ context 'to all issues' do
+ before do
+ check 'check_all_issues'
+ open_labels_dropdown ['bug', 'feature']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+ end
+ end
+
+ context 'to a issue' do
+ before do
+ check "selected_issue_#{issue1.id}"
+ open_labels_dropdown ['bug', 'feature']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
+ end
+ end
+ end
+ end
+
+ context 'can assign a label to all issues when label is present' do
+ before do
+ issue2.labels << bug
+ issue2.labels << feature
+ visit namespace_project_issues_path(project.namespace, project)
+
+ check 'check_all_issues'
+ open_labels_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'bug'
+ end
+ end
+
+ context 'can bulk un-assign' do
+ context 'all labels to all issues' do
+ before do
+ issue1.labels << bug
+ issue1.labels << feature
+ issue2.labels << bug
+ issue2.labels << feature
+
+ visit namespace_project_issues_path(project.namespace, project)
+
+ check 'check_all_issues'
+ unmark_labels_in_dropdown ['bug', 'feature']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
+ end
+ end
+
+ context 'a label to a issue' do
+ before do
+ issue1.labels << bug
+ issue2.labels << feature
+
+ visit namespace_project_issues_path(project.namespace, project)
+
+ check_issue issue1
+ unmark_labels_in_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+ end
+ end
+
+ context 'a label and keep the others label' do
+ before do
+ issue1.labels << bug
+ issue1.labels << feature
+ issue2.labels << bug
+ issue2.labels << feature
+
+ visit namespace_project_issues_path(project.namespace, project)
+
+ check_issue issue1
+ check_issue issue2
+ unmark_labels_in_dropdown ['bug']
+ update_issues
+ end
+
+ it do
+ expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+ expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+ expect(find("#issue_#{issue2.id}")).to have_content 'feature'
+ end
+ end
+ end
+ end
+
+ context 'as a guest' do
+ before do
+ login_as user
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ context 'cannot bulk assign labels' do
+ it do
+ expect(page).not_to have_css '.check_all_issues'
+ expect(page).not_to have_css '.issue-check'
+ end
+ end
+ end
+
+ def open_labels_dropdown(items = [], unmark = false)
+ page.within('.issues_bulk_update') do
+ click_button 'Label'
+ wait_for_ajax
+ items.map do |item|
+ click_link item
+ end
+ if unmark
+ items.map do |item|
+ click_link item
+ end
+ end
+ end
+ end
+
+ def unmark_labels_in_dropdown(items = [])
+ open_labels_dropdown(items, true)
+ end
+
+ def check_issue(issue)
+ page.within('.issues-list') do
+ check "selected_issue_#{issue.id}"
+ end
+ end
+
+ def update_issues
+ click_button 'Update issues'
+ wait_for_ajax
+ end
+end
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index 7f654684143..5ea02b8d39c 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -54,6 +54,12 @@ feature 'Issue filtering by Labels', feature: true do
expect(find('.filtered-labels')).not_to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end
+
+ it 'should remove label "bug"' do
+ find('.js-label-filter-remove').click
+ wait_for_ajax
+ expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
+ end
end
context 'filter by label feature', js: true do
@@ -135,6 +141,12 @@ feature 'Issue filtering by Labels', feature: true do
it 'should not show label "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
end
+
+ it 'should remove label "enhancement"' do
+ find('.js-label-filter-remove', match: :first).click
+ wait_for_ajax
+ expect(find('.filtered-labels')).to have_no_content "enhancement"
+ end
end
context 'filter by label enhancement and bug in issues list', js: true do
@@ -164,4 +176,42 @@ feature 'Issue filtering by Labels', feature: true do
expect(find('.filtered-labels')).not_to have_content "feature"
end
end
+
+ context 'remove filtered labels', js: true do
+ before do
+ page.within '.labels-filter' do
+ click_button 'Label'
+ wait_for_ajax
+ click_link 'bug'
+ find('.dropdown-menu-close').click
+ end
+
+ page.within '.filtered-labels' do
+ expect(page).to have_content 'bug'
+ end
+ end
+
+ it 'should allow user to remove filtered labels' do
+ first('.js-label-filter-remove').click
+ wait_for_ajax
+
+ expect(find('.filtered-labels', visible: false)).not_to have_content 'bug'
+ expect(find('.labels-filter')).not_to have_content 'bug'
+ end
+ end
+
+ context 'dropdown filtering', js: true do
+ it 'should filter by label name' do
+ page.within '.labels-filter' do
+ click_button 'Label'
+ wait_for_ajax
+ fill_in 'label-name', with: 'bug'
+
+ page.within '.dropdown-content' do
+ expect(page).not_to have_content 'enhancement'
+ expect(page).to have_content 'bug'
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 192e3619375..4bcb105b17d 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Filter issues', feature: true do
+ include WaitForAjax
let!(:project) { create(:project) }
let!(:user) { create(:user)}
@@ -21,7 +22,7 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-user-link', text: user.username).click
- sleep 2
+ wait_for_ajax
end
context 'assignee', js: true do
@@ -53,7 +54,7 @@ describe 'Filter issues', feature: true do
find('.milestone-filter .dropdown-content a', text: milestone.title).click
- sleep 2
+ wait_for_ajax
end
context 'milestone', js: true do
@@ -80,23 +81,21 @@ describe 'Filter issues', feature: true do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-label-select').click
+ wait_for_ajax
end
it 'should filter by any label' do
find('.dropdown-menu-labels a', text: 'Any Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
+ wait_for_ajax
- page.within '.labels-filter' do
- expect(page).to have_content 'Any Label'
- end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Any Label')
+ expect(find('.labels-filter')).to have_content 'Label'
end
it 'should filter by no label' do
find('.dropdown-menu-labels a', text: 'No Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
+ wait_for_ajax
page.within '.labels-filter' do
expect(page).to have_content 'No Label'
@@ -122,14 +121,14 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-user-link', text: user.username).click
- sleep 2
+ wait_for_ajax
find('.js-label-select').click
find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- sleep 2
+ wait_for_ajax
end
context 'assignee and label', js: true do
@@ -154,4 +153,148 @@ describe 'Filter issues', feature: true do
end
end
end
+
+ describe 'filter issues by text' do
+ before do
+ create(:issue, title: "Bug", project: project)
+
+ bug_label = create(:label, project: project, title: 'bug')
+ milestone = create(:milestone, title: "8", project: project)
+
+ issue = create(:issue,
+ title: "Bug 2",
+ project: project,
+ milestone: milestone,
+ author: user,
+ assignee: user)
+ issue.labels << bug_label
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ context 'only text', js: true do
+ it 'should filter issues by searched text' do
+ fill_in 'issue_search', with: 'Bug'
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+ end
+
+ it 'should not show any issues' do
+ fill_in 'issue_search', with: 'testing'
+
+ page.within '.issues-list' do
+ expect(page).not_to have_selector('.issue')
+ end
+ end
+ end
+
+ context 'text and dropdown options', js: true do
+ it 'should filter by text and label' do
+ fill_in 'issue_search', with: 'Bug'
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+
+ click_button 'Label'
+ page.within '.labels-filter' do
+ click_link 'bug'
+ end
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 1)
+ end
+ end
+
+ it 'should filter by text and milestone' do
+ fill_in 'issue_search', with: 'Bug'
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+
+ click_button 'Milestone'
+ page.within '.milestone-filter' do
+ click_link '8'
+ end
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 1)
+ end
+ end
+
+ it 'should filter by text and assignee' do
+ fill_in 'issue_search', with: 'Bug'
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+
+ click_button 'Assignee'
+ page.within '.dropdown-menu-assignee' do
+ click_link user.name
+ end
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 1)
+ end
+ end
+
+ it 'should filter by text and author' do
+ fill_in 'issue_search', with: 'Bug'
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+
+ click_button 'Author'
+ page.within '.dropdown-menu-author' do
+ click_link user.name
+ end
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 1)
+ end
+ end
+ end
+ end
+
+ describe 'filter issues and sort', js: true do
+ before do
+ bug_label = create(:label, project: project, title: 'bug')
+ bug_one = create(:issue, title: "Frontend", project: project)
+ bug_two = create(:issue, title: "Bug 2", project: project)
+
+ bug_one.labels << bug_label
+ bug_two.labels << bug_label
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ it 'should be able to filter and sort issues' do
+ click_button 'Label'
+ wait_for_ajax
+ page.within '.labels-filter' do
+ click_link 'bug'
+ end
+ find('.dropdown-menu-close-icon').click
+ wait_for_ajax
+
+ page.within '.issues-list' do
+ expect(page).to have_selector('.issue', count: 2)
+ end
+
+ click_button 'Last created'
+ page.within '.dropdown-menu-sort' do
+ click_link 'Oldest created'
+ end
+ wait_for_ajax
+
+ page.within '.issues-list' do
+ expect(first('.issue')).to have_content('Frontend')
+ end
+ end
+ end
end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 84c8e20ebaa..c7019c5aea1 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -19,7 +19,7 @@ feature 'issue move to another project' do
end
scenario 'moving issue to another project not allowed' do
- expect(page).to have_no_select('move_to_project_id')
+ expect(page).to have_no_selector('#move_to_project_id')
end
end
@@ -37,7 +37,7 @@ feature 'issue move to another project' do
end
scenario 'moving issue to another project' do
- select(new_project.name_with_namespace, from: 'move_to_project_id')
+ first('#move_to_project_id', visible: false).set(new_project.id)
click_button('Save changes')
expect(current_url).to include project_path(new_project)
@@ -47,14 +47,18 @@ feature 'issue move to another project' do
expect(page).to have_content(issue.title)
end
- context 'projects user does not have permission to move issue to exist' do
+ context 'user does not have permission to move the issue to a project', js: true do
let!(:private_project) { create(:project, :private) }
let(:another_project) { create(:project) }
background { another_project.team << [user, :guest] }
scenario 'browsing projects in projects select' do
- options = [ '', 'No project', new_project.name_with_namespace ]
- expect(page).to have_select('move_to_project_id', options: options)
+ click_link 'Select project'
+
+ page.within '.select2-results' do
+ expect(page).to have_content 'No project'
+ expect(page).to have_content new_project.name_with_namespace
+ end
end
end
@@ -65,7 +69,7 @@ feature 'issue move to another project' do
end
scenario 'user wants to move issue that has already been moved' do
- expect(page).to have_no_select('move_to_project_id')
+ expect(page).to have_no_selector('#move_to_project_id')
end
end
end
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index e4efdbe2421..f5cfe2d666e 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -9,8 +9,11 @@ feature 'Issue notes polling' do
end
scenario 'Another user adds a comment to an issue', js: true do
- note = create(:note_on_issue, noteable: issue, note: 'Looks good!')
+ note = create(:note, noteable: issue, project: project,
+ note: 'Looks good!')
+
page.execute_script('notes.refresh();')
+
expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!')
end
end
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
new file mode 100644
index 00000000000..b69cce3e7d7
--- /dev/null
+++ b/spec/features/issues/todo_spec.rb
@@ -0,0 +1,33 @@
+require 'rails_helper'
+
+feature 'Manually create a todo item from issue', feature: true, js: true do
+ let!(:project) { create(:project) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:user) { create(:user)}
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should create todo when clicking button' do
+ page.within '.issuable-sidebar' do
+ click_button 'Add Todo'
+ expect(page).to have_content 'Mark Done'
+ end
+
+ page.within '.header-content .todos-pending-count' do
+ expect(page).to have_content '1'
+ end
+ end
+
+ it 'should mark a todo as done' do
+ page.within '.issuable-sidebar' do
+ click_button 'Add Todo'
+ click_button 'Mark Done'
+ end
+
+ expect(page).to have_selector('.todos-pending-count', visible: false)
+ end
+end
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index b03dd0f666d..ddbd69b2891 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
feature 'Multiple issue updating from issues#index', feature: true do
+ include WaitForAjax
+
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
@@ -24,9 +26,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'should be set to open' do
create_closed
- visit namespace_project_issues_path(project.namespace, project)
-
- find('.issues-state-filters a', text: 'Closed').click
+ visit namespace_project_issues_path(project.namespace, project, state: 'closed')
find('#check_all_issues').click
find('.js-issue-status').click
@@ -42,7 +42,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
- find('.js-update-assignee').click
+ click_update_assignee_button
find('.dropdown-menu-user-link', text: user.username).click
click_update_issues_button
@@ -57,14 +57,11 @@ feature 'Multiple issue updating from issues#index', feature: true do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
- find('.js-update-assignee').click
+ click_update_assignee_button
click_link 'Unassigned'
click_update_issues_button
-
- within first('.issue .controls') do
- expect(page).to have_no_selector('.author_link')
- end
+ expect(find('.issue:first-child .controls')).not_to have_css('.author_link')
end
end
@@ -95,7 +92,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
find('.dropdown-menu-milestone a', text: "No Milestone").click
click_update_issues_button
- expect(first('.issue')).to_not have_content milestone.title
+ expect(find('.issue:first-child')).not_to have_content milestone.title
end
end
@@ -111,7 +108,13 @@ feature 'Multiple issue updating from issues#index', feature: true do
create(:issue, project: project, milestone: milestone)
end
+ def click_update_assignee_button
+ find('.js-update-assignee').click
+ wait_for_ajax
+ end
+
def click_update_issues_button
find('.update_selected_issues').click
+ wait_for_ajax
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index d5755c293c5..f6fb6a72d22 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -64,10 +64,70 @@ describe 'Issues', feature: true do
end
end
+ describe 'due date', js: true do
+ context 'on new form' do
+ before do
+ visit new_namespace_project_issue_path(project.namespace, project)
+ end
+
+ it 'should save with due date' do
+ date = Date.today.at_beginning_of_month
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+ find('#issuable-due-date').click
+
+ page.within '.ui-datepicker' do
+ click_link date.day
+ end
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ click_button 'Submit issue'
+
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content date.to_s(:medium)
+ end
+ end
+ end
+
+ context 'on edit form' do
+ let(:issue) { create(:issue, author: @user,project: project, due_date: Date.today.at_beginning_of_month.to_s) }
+
+ before do
+ visit edit_namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should save with due date' do
+ date = Date.today.at_beginning_of_month
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ date = date.tomorrow
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+ find('#issuable-due-date').click
+
+ page.within '.ui-datepicker' do
+ click_link date.day
+ end
+
+ expect(find('#issuable-due-date').value).to eq date.to_s
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ expect(page).to have_content date.to_s(:medium)
+ end
+ end
+ end
+ end
+
describe 'Issue info' do
it 'excludes award_emoji from comment count' do
issue = create(:issue, author: @user, assignee: @user, project: project, title: 'foobar')
- create(:upvote_note, noteable: issue)
+ create(:award_emoji, awardable: issue)
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
@@ -307,13 +367,9 @@ describe 'Issues', feature: true do
page.within('.assignee') do
expect(page).to have_content "#{@user.name}"
- end
- find('.block.assignee .edit-link').click
- sleep 2 # wait for ajax stuff to complete
- first('.dropdown-menu-user-link').click
- sleep 2
- page.within('.assignee') do
+ click_link 'Edit'
+ click_link 'Unassigned'
expect(page).to have_content 'No assignee'
end
@@ -331,7 +387,7 @@ describe 'Issues', feature: true do
page.within '.assignee' do
click_link 'Edit'
end
-
+
page.within '.dropdown-menu-user' do
click_link @user.name
end
@@ -431,6 +487,43 @@ describe 'Issues', feature: true do
end
end
+ describe 'due date' do
+ context 'update due on issue#show', js: true do
+ let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'should add due date to issue' do
+ page.within '.due_date' do
+ click_link 'Edit'
+
+ page.within '.ui-datepicker-calendar' do
+ first('.ui-state-default').click
+ end
+
+ expect(page).to have_no_content 'None'
+ end
+ end
+
+ it 'should remove due date from issue' do
+ page.within '.due_date' do
+ click_link 'Edit'
+
+ page.within '.ui-datepicker-calendar' do
+ first('.ui-state-default').click
+ end
+
+ expect(page).to have_no_content 'None'
+
+ click_link 'remove due date'
+ expect(page).to have_content 'None'
+ end
+ end
+ end
+ end
+
def first_issue
page.all('ul.issues-list > li').first.text
end
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 8c38dd5b122..72b5ff231f7 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -32,12 +32,12 @@ feature 'Login', feature: true do
let(:user) { create(:user, :two_factor) }
before do
- login_with(user)
- expect(page).to have_content('Two-factor Authentication')
+ login_with(user, remember: true)
+ expect(page).to have_content('Two-Factor Authentication')
end
def enter_code(code)
- fill_in 'Two-factor Authentication code', with: code
+ fill_in 'Two-Factor Authentication code', with: code
click_button 'Verify code'
end
@@ -52,6 +52,12 @@ feature 'Login', feature: true do
expect(current_path).to eq root_path
end
+ it 'persists remember_me value via hidden field' do
+ field = first('input#user_remember_me', visible: false)
+
+ expect(field.value).to eq '1'
+ end
+
it 'blocks login with invalid code' do
enter_code('foo')
expect(page).to have_content('Invalid two-factor code')
@@ -121,7 +127,7 @@ feature 'Login', feature: true do
user = create(:user, password: 'not-the-default')
login_with(user)
- expect(page).to have_content('Invalid login or password.')
+ expect(page).to have_content('Invalid Login or password.')
end
end
@@ -137,12 +143,12 @@ feature 'Login', feature: true do
context 'within the grace period' do
it 'redirects to two-factor configuration page' do
- expect(current_path).to eq new_profile_two_factor_auth_path
- expect(page).to have_content('You must enable Two-factor Authentication for your account before')
+ expect(current_path).to eq profile_two_factor_auth_path
+ expect(page).to have_content('You must enable Two-Factor Authentication for your account before')
end
- it 'disallows skipping two-factor configuration' do
- expect(current_path).to eq new_profile_two_factor_auth_path
+ it 'allows skipping two-factor configuration', js: true do
+ expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
expect(current_path).to eq root_path
@@ -153,26 +159,26 @@ feature 'Login', feature: true do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
it 'redirects to two-factor configuration page' do
- expect(current_path).to eq new_profile_two_factor_auth_path
- expect(page).to have_content('You must enable Two-factor Authentication for your account.')
+ expect(current_path).to eq profile_two_factor_auth_path
+ expect(page).to have_content('You must enable Two-Factor Authentication for your account.')
end
- it 'disallows skipping two-factor configuration' do
- expect(current_path).to eq new_profile_two_factor_auth_path
+ it 'disallows skipping two-factor configuration', js: true do
+ expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
end
end
- context 'without grace pariod defined' do
+ context 'without grace period defined' do
before(:each) do
stub_application_setting(two_factor_grace_period: 0)
login_with(user)
end
it 'redirects to two-factor configuration page' do
- expect(current_path).to eq new_profile_two_factor_auth_path
- expect(page).to have_content('You must enable Two-factor Authentication for your account.')
+ expect(current_path).to eq profile_two_factor_auth_path
+ expect(page).to have_content('You must enable Two-Factor Authentication for your account.')
end
end
end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 0148c87084a..09ccc77c101 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -165,22 +165,32 @@ describe 'GitLab Markdown', feature: true do
describe 'ExternalLinkFilter' do
it 'adds nofollow to external link' do
link = doc.at_css('a:contains("Google")')
+
expect(link.attr('rel')).to include('nofollow')
end
it 'adds noreferrer to external link' do
link = doc.at_css('a:contains("Google")')
+
expect(link.attr('rel')).to include('noreferrer')
end
+ it 'adds _blank to target attribute for external links' do
+ link = doc.at_css('a:contains("Google")')
+
+ expect(link.attr('target')).to match('_blank')
+ end
+
it 'ignores internal link' do
link = doc.at_css('a:contains("GitLab Root")')
+
expect(link.attr('rel')).not_to match 'nofollow'
+ expect(link.attr('target')).not_to match '_blank'
end
end
end
- before(:all) do
+ before do
@feat = MarkdownFeature.new
# `markdown` helper expects a `@project` variable
@@ -188,7 +198,7 @@ describe 'GitLab Markdown', feature: true do
end
context 'default pipeline' do
- before(:all) do
+ before do
@html = markdown(@feat.raw_markdown)
end
@@ -231,13 +241,14 @@ describe 'GitLab Markdown', feature: true do
context 'wiki pipeline' do
before do
@project_wiki = @feat.project_wiki
+ @project_wiki_page = @feat.project_wiki_page
file = Gollum::File.new(@project_wiki.wiki)
expect(file).to receive(:path).and_return('images/example.jpg')
expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file)
allow(@project_wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' }
- @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki })
+ @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki, page_slug: @project_wiki_page.slug })
end
it_behaves_like 'all pipelines'
@@ -278,6 +289,10 @@ describe 'GitLab Markdown', feature: true do
it 'includes GollumTagsFilter' do
expect(doc).to parse_gollum_tags
end
+
+ it 'includes InlineDiffFilter' do
+ expect(doc).to parse_inline_diffs
+ end
end
# Fake a `current_user` helper
diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb
new file mode 100644
index 00000000000..007f67d6080
--- /dev/null
+++ b/spec/features/merge_requests/award_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+feature 'Merge request awards', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ describe 'logged in' do
+ before do
+ login_as(user)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'should add award to merge request' do
+ first('.js-emoji-btn').click
+ expect(page).to have_selector('.js-emoji-btn.active')
+ expect(first('.js-emoji-btn')).to have_content '1'
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ expect(first('.js-emoji-btn')).to have_content '1'
+ end
+
+ it 'should remove award from merge request' do
+ first('.js-emoji-btn').click
+ find('.js-emoji-btn.active').click
+ expect(first('.js-emoji-btn')).to have_content '0'
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ expect(first('.js-emoji-btn')).to have_content '0'
+ end
+
+ it 'should only have one menu on the page' do
+ first('.js-add-award').click
+ expect(page).to have_selector('.emoji-menu')
+
+ expect(page).to have_selector('.emoji-menu', count: 1)
+ end
+ end
+
+ describe 'logged out' do
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'should not see award menu button' do
+ expect(page).not_to have_selector('.js-award-holder')
+ end
+ end
+end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
new file mode 100644
index 00000000000..b4d2201c729
--- /dev/null
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+feature 'Merge request created from fork' do
+ given(:user) { create(:user) }
+ given(:project) { create(:project, :public) }
+ given(:fork_project) { create(:project, :public) }
+
+ given!(:merge_request) do
+ create(:forked_project_link, forked_to_project: fork_project,
+ forked_from_project: project)
+
+ create(:merge_request_with_diffs, source_project: fork_project,
+ target_project: project,
+ description: 'Test merge request')
+ end
+
+ background do
+ fork_project.team << [user, :master]
+ login_as user
+ end
+
+ scenario 'user can access merge request' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_content 'Test merge request'
+ end
+
+ context 'pipeline present in source project' do
+ include WaitForAjax
+
+ given(:pipeline) do
+ create(:ci_pipeline_with_two_job, project: fork_project,
+ sha: merge_request.last_commit.id,
+ ref: merge_request.source_branch)
+ end
+
+ background { pipeline.create_builds(user) }
+
+ scenario 'user visits a pipelines page', js: true do
+ visit_merge_request(merge_request)
+ page.within('.merge-request-tabs') { click_link 'Builds' }
+ wait_for_ajax
+
+ page.within('table.builds') do
+ expect(page).to have_content 'rspec'
+ expect(page).to have_content 'spinach'
+ end
+
+ expect(find_link('Cancel running')[:href])
+ .to include fork_project.path_with_namespace
+ end
+ end
+
+ def visit_merge_request(mr)
+ visit namespace_project_merge_request_path(project.namespace,
+ project, mr)
+ end
+end
diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
index 7aa7eb965e9..c5e6412d7bf 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -12,8 +12,8 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
end
context "Active build for Merge Request" do
- let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) }
- let!(:ci_build) { create(:ci_build, commit: ci_commit) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) }
+ let!(:ci_build) { create(:ci_build, pipeline: pipeline) }
before do
login_as user
@@ -47,8 +47,8 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
merge_user: user, title: "MepMep", merge_when_build_succeeds: true)
end
- let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) }
- let!(:ci_build) { create(:ci_build, commit: ci_commit) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) }
+ let!(:ci_build) { create(:ci_build, pipeline: pipeline) }
before do
login_as user
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb
new file mode 100644
index 00000000000..65e9185ec24
--- /dev/null
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+feature 'Only allow merge requests to be merged if the build succeeds', feature: true do
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project) }
+
+ before do
+ login_as merge_request.author
+
+ project.team << [merge_request.author, :master]
+ end
+
+ context 'project does not have CI enabled' do
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+
+ context 'when project has CI enabled' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch) }
+
+ context 'when merge requests can only be merged if the build succeeds' do
+ before do
+ project.update_attribute(:only_allow_merge_if_build_succeeds, true)
+ end
+
+ context 'when CI is running' do
+ before { pipeline.update_column(:status, :running) }
+
+ it 'does not allow to merge immediately' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Merge When Build Succeeds'
+ expect(page).not_to have_button 'Select Merge Moment'
+ end
+ end
+
+ context 'when CI failed' do
+ before { pipeline.update_column(:status, :failed) }
+
+ it 'does not allow MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).not_to have_button 'Accept Merge Request'
+ expect(page).to have_content('Please retry the build or push a new commit to fix the failure.')
+ end
+ end
+
+ context 'when CI succeeded' do
+ before { pipeline.update_column(:status, :success) }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+ end
+
+ context 'when merge requests can be merged when the build failed' do
+ before do
+ project.update_attribute(:only_allow_merge_if_build_succeeds, false)
+ end
+
+ context 'when CI is running' do
+ before { pipeline.update_column(:status, :running) }
+
+ it 'allows MR to be merged immediately', js: true do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Merge When Build Succeeds'
+
+ click_button 'Select Merge Moment'
+ expect(page).to have_content 'Merge Immediately'
+ end
+ end
+
+ context 'when CI failed' do
+ before { pipeline.update_column(:status, :failed) }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+
+ context 'when CI succeeded' do
+ before { pipeline.update_column(:status, :success) }
+
+ it 'allows MR to be merged' do
+ visit_merge_request(merge_request)
+
+ expect(page).to have_button 'Accept Merge Request'
+ end
+ end
+ end
+ end
+
+ def visit_merge_request(merge_request)
+ visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
+ end
+end
diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
index 2c7e1c748ad..1c130057c56 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -131,6 +131,15 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
expect(first_merge_request).to include('fix')
expect(count_merge_requests).to eq(1)
end
+
+ it 'sorts by recently due milestone' do
+ visit namespace_project_merge_requests_path(project.namespace, project,
+ label_name: [label.name, label2.name],
+ assignee_id: user.id,
+ sort: sort_value_milestone_soon)
+
+ expect(first_merge_request).to include('fix')
+ end
end
end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 9e9fec01943..737efcef45d 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -4,25 +4,15 @@ describe 'Comments', feature: true do
include RepoHelpers
include WaitForAjax
- describe 'On merge requests page', feature: true do
- it 'excludes award_emoji from comment count' do
- merge_request = create(:merge_request)
- project = merge_request.source_project
- create(:upvote_note, noteable: merge_request, project: project)
-
- login_as :admin
- visit namespace_project_merge_requests_path(project.namespace, project)
-
- expect(merge_request.mr_and_commit_notes.count).to eq 1
- expect(page.all('.merge-request-no-comments').first.text).to eq "0"
+ describe 'On a merge request', js: true, feature: true do
+ let!(:project) { create(:project) }
+ let!(:merge_request) do
+ create(:merge_request, source_project: project, target_project: project)
end
- end
- describe 'On a merge request', js: true, feature: true do
- let!(:merge_request) { create(:merge_request) }
- let!(:project) { merge_request.source_project }
let!(:note) do
- create(:note_on_merge_request, :with_attachment, project: project)
+ create(:note_on_merge_request, :with_attachment, noteable: merge_request,
+ project: project)
end
before do
@@ -143,17 +133,6 @@ describe 'Comments', feature: true do
end
end
end
-
- describe 'comment info' do
- it 'excludes award_emoji from comment count' do
- create(:upvote_note, noteable: merge_request, project: project)
-
- visit namespace_project_merge_request_path(project.namespace, project, merge_request)
-
- expect(merge_request.mr_and_commit_notes.count).to eq 2
- expect(find('.notes-tab span.badge').text).to eq "1"
- end
- end
end
describe 'On a merge request diff', js: true, feature: true do
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index 1adab7e9c6c..c7c00a3266a 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -32,7 +32,8 @@ feature 'Member autocomplete', feature: true do
context 'adding a new note on a Issue', js: true do
before do
issue = create(:issue, author: author, project: project)
- create(:note, note: 'Ultralight Beam', noteable: issue, author: participant)
+ create(:note, note: 'Ultralight Beam', noteable: issue,
+ project: project, author: participant)
visit_issue(project, issue)
end
@@ -47,7 +48,8 @@ feature 'Member autocomplete', feature: true do
context 'adding a new note on a Merge Request ', js: true do
before do
merge = create(:merge_request, source_project: project, target_project: project, author: author)
- create(:note, note: 'Ultralight Beam', noteable: merge, author: participant)
+ create(:note, note: 'Ultralight Beam', noteable: merge,
+ project: project, author: participant)
visit_merge_request(project, merge)
end
diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb
new file mode 100644
index 00000000000..98703ef3ac4
--- /dev/null
+++ b/spec/features/pipelines_spec.rb
@@ -0,0 +1,189 @@
+require 'spec_helper'
+
+describe "Pipelines" do
+ include GitlabRoutingHelper
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ project.team << [user, :developer]
+ end
+
+ describe 'GET /:project/pipelines' do
+ let!(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', status: 'running') }
+
+ [:all, :running, :branches].each do |scope|
+ context "displaying #{scope}" do
+ let(:project) { create(:project) }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) }
+
+ it { expect(page).to have_content(pipeline.short_sha) }
+ end
+ end
+
+ context 'anonymous access' do
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_http_status(:success) }
+ end
+
+ context 'cancelable pipeline' do
+ let!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_link('Cancel') }
+ it { expect(page).to have_selector('.ci-running') }
+
+ context 'when canceling' do
+ before { click_link('Cancel') }
+
+ it { expect(page).not_to have_link('Cancel') }
+ it { expect(page).to have_selector('.ci-canceled') }
+ end
+ end
+
+ context 'retryable pipelines' do
+ let!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_link('Retry') }
+ it { expect(page).to have_selector('.ci-failed') }
+
+ context 'when retrying' do
+ before { click_link('Retry') }
+
+ it { expect(page).not_to have_link('Retry') }
+ it { expect(page).to have_selector('.ci-pending') }
+ end
+ end
+
+ context 'for generic statuses' do
+ context 'when running' do
+ let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it 'not be cancelable' do
+ expect(page).not_to have_link('Cancel')
+ end
+
+ it 'pipeline is running' do
+ expect(page).to have_selector('.ci-running')
+ end
+ end
+
+ context 'when failed' do
+ let!(:running) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it 'not be retryable' do
+ expect(page).not_to have_link('Retry')
+ end
+
+ it 'pipeline is failed' do
+ expect(page).to have_selector('.ci-failed')
+ end
+ end
+ end
+
+ context 'downloadable pipelines' do
+ context 'with artifacts' do
+ let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') }
+
+ before { visit namespace_project_pipelines_path(project.namespace, project) }
+
+ it { expect(page).to have_selector('.build-artifacts') }
+ it { expect(page).to have_link(with_artifacts.name) }
+ end
+
+ context 'without artifacts' do
+ let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
+
+ it { expect(page).not_to have_selector('.build-artifacts') }
+ end
+ end
+ end
+
+ describe 'GET /:project/pipelines/:id' do
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
+
+ before do
+ @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build')
+ @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test')
+ @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy')
+ @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external')
+ end
+
+ before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) }
+
+ it 'showing a list of builds' do
+ expect(page).to have_content('Tests')
+ expect(page).to have_content(@success.id)
+ expect(page).to have_content('Deploy')
+ expect(page).to have_content(@failed.id)
+ expect(page).to have_content(@running.id)
+ expect(page).to have_content(@external.id)
+ expect(page).to have_content('Retry failed')
+ expect(page).to have_content('Cancel running')
+ end
+
+ context 'retrying builds' do
+ it { expect(page).not_to have_content('retried') }
+
+ context 'when retrying' do
+ before { click_on 'Retry failed' }
+
+ it { expect(page).not_to have_content('Retry failed') }
+ it { expect(page).to have_content('retried') }
+ end
+ end
+
+ context 'canceling builds' do
+ it { expect(page).not_to have_selector('.ci-canceled') }
+
+ context 'when canceling' do
+ before { click_on 'Cancel running' }
+
+ it { expect(page).not_to have_content('Cancel running') }
+ it { expect(page).to have_selector('.ci-canceled') }
+ end
+ end
+ end
+
+ describe 'POST /:project/pipelines' do
+ let(:project) { create(:project) }
+
+ before { visit new_namespace_project_pipeline_path(project.namespace, project) }
+
+ context 'for valid commit' do
+ before { fill_in('Create for', with: 'master') }
+
+ context 'with gitlab-ci.yml' do
+ before { stub_ci_pipeline_to_return_yaml_file }
+
+ it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) }
+ end
+
+ context 'without gitlab-ci.yml' do
+ before { click_on 'Create pipeline' }
+
+ it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
+ end
+ end
+
+ context 'for invalid commit' do
+ before do
+ fill_in('Create for', with: 'invalid reference')
+ click_on 'Create pipeline'
+ end
+
+ it { expect(page).to have_content('Reference not found') }
+ end
+ end
+end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index 8f645438cff..787bf42d048 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -54,7 +54,7 @@ describe 'Profile > Preferences', feature: true do
end
end
- describe 'User changes their default dashboard' do
+ describe 'User changes their default dashboard', js: true do
it 'creates a flash message' do
select 'Starred Projects', from: 'user_dashboard'
click_button 'Save'
@@ -66,8 +66,10 @@ describe 'Profile > Preferences', feature: true do
select 'Starred Projects', from: 'user_dashboard'
click_button 'Save'
- click_link 'Dashboard'
- expect(page.current_path).to eq starred_dashboard_projects_path
+ allowing_for_delay do
+ find('#logo').click
+ expect(page.current_path).to eq starred_dashboard_projects_path
+ end
click_link 'Your Projects'
expect(page.current_path).to eq dashboard_projects_path
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 13c9b95b316..51be81d634c 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -8,12 +8,10 @@ feature 'list of badges' do
project = create(:project)
project.team << [user, :master]
login_as(user)
- visit edit_namespace_project_path(project.namespace, project)
+ visit namespace_project_badges_path(project.namespace, project)
end
scenario 'user displays list of badges' do
- click_link 'Badges'
-
expect(page).to have_content 'build status'
expect(page).to have_content 'Markdown'
expect(page).to have_content 'HTML'
@@ -26,7 +24,6 @@ feature 'list of badges' do
end
scenario 'user changes current ref on badges list page', js: true do
- click_link 'Badges'
select2('improve/awesome', from: '#ref')
expect(page).to have_content 'badges/improve/awesome/build.svg'
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index 40ba0bdc115..15c381c0f5a 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -11,9 +11,9 @@ feature 'project commit builds' do
context 'when no builds triggered yet' do
background do
- create(:ci_commit, project: project,
- sha: project.commit.sha,
- ref: 'master')
+ create(:ci_pipeline, project: project,
+ sha: project.commit.sha,
+ ref: 'master')
end
scenario 'user views commit builds page' do
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index 0559b02f321..f88c0616b52 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -16,6 +16,7 @@ describe 'Cherry-pick Commits' do
it do
visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
+ expect(page).not_to have_content('v1.0.0') # Only branches, not tags
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
click_button 'Cherry-pick'
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
new file mode 100644
index 00000000000..073a83b6896
--- /dev/null
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+feature 'User wants to add a .gitignore file', feature: true do
+ include WaitForAjax
+
+ before do
+ user = create(:user)
+ project = create(:project)
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitignore')
+ end
+
+ scenario 'user can see .gitignore dropdown' do
+ expect(page).to have_css('.gitignore-selector')
+ end
+
+ scenario 'user can pick a .gitignore file from the dropdown', js: true do
+ find('.js-gitignore-selector').click
+ wait_for_ajax
+ within '.gitignore-selector' do
+ find('.dropdown-input-field').set('rails')
+ find('.dropdown-content li', text: 'Rails').click
+ end
+ wait_for_ajax
+
+ expect(page).to have_content('/.bundle')
+ expect(page).to have_content('# Gemfile.lock, .ruby-version, .ruby-gemset')
+ end
+end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 3d6ffbc4c6b..ecc818eb1e1 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -25,7 +25,7 @@ feature 'project owner creates a license file', feature: true, js: true do
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
- expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
click_button 'Commit Changes'
@@ -33,7 +33,7 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
expect(page).to have_content('The MIT License (MIT)')
- expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
scenario 'project master creates a license file from the "Add license" link' do
@@ -48,7 +48,7 @@ feature 'project owner creates a license file', feature: true, js: true do
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
- expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
click_button 'Commit Changes'
@@ -56,6 +56,6 @@ feature 'project owner creates a license file', feature: true, js: true do
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
expect(page).to have_content('The MIT License (MIT)')
- expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
end
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 3268e240200..34eda29c285 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -24,7 +24,7 @@ feature 'project owner sees a link to create a license file in empty project', f
file_content = find('.file-content')
expect(file_content).to have_content('The MIT License (MIT)')
- expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
# Remove pre-receive hook so we can push without auth
@@ -34,6 +34,6 @@ feature 'project owner sees a link to create a license file in empty project', f
expect(current_path).to eq(
namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
expect(page).to have_content('The MIT License (MIT)')
- expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ expect(page).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
end
end
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
new file mode 100644
index 00000000000..461f1737928
--- /dev/null
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+feature 'Issue prioritization', feature: true do
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+
+ # Labels
+ let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
+ let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
+ let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
+ let(:label_4) { create(:label, title: 'label_4', project: project, priority: 4) }
+ let(:label_5) { create(:label, title: 'label_5', project: project) } # no priority
+
+ # According to https://gitlab.com/gitlab-org/gitlab-ce/issues/14189#note_4360653
+ context 'when issues have one label' do
+ scenario 'Are sorted properly' do
+
+ # Issues
+ issue_1 = create(:issue, title: 'issue_1', project: project)
+ issue_2 = create(:issue, title: 'issue_2', project: project)
+ issue_3 = create(:issue, title: 'issue_3', project: project)
+ issue_4 = create(:issue, title: 'issue_4', project: project)
+ issue_5 = create(:issue, title: 'issue_5', project: project)
+
+ # Assign labels to issues disorderly
+ issue_4.labels << label_1
+ issue_3.labels << label_2
+ issue_5.labels << label_3
+ issue_2.labels << label_4
+ issue_1.labels << label_5
+
+ login_as user
+ visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
+
+ # Ensure we are indicating that issues are sorted by priority
+ expect(page).to have_selector('.dropdown-toggle', text: 'Priority')
+
+ page.within('.issues-holder') do
+ issue_titles = all('.issues-list .issue-title-text').map(&:text)
+
+ expect(issue_titles).to eq(['issue_4', 'issue_3', 'issue_5', 'issue_2', 'issue_1'])
+ end
+ end
+ end
+
+ context 'when issues have multiple labels' do
+ scenario 'Are sorted properly' do
+
+ # Issues
+ issue_1 = create(:issue, title: 'issue_1', project: project)
+ issue_2 = create(:issue, title: 'issue_2', project: project)
+ issue_3 = create(:issue, title: 'issue_3', project: project)
+ issue_4 = create(:issue, title: 'issue_4', project: project)
+ issue_5 = create(:issue, title: 'issue_5', project: project)
+ issue_6 = create(:issue, title: 'issue_6', project: project)
+ issue_7 = create(:issue, title: 'issue_7', project: project)
+ issue_8 = create(:issue, title: 'issue_8', project: project)
+
+ # Assign labels to issues disorderly
+ issue_5.labels << label_1 # 1
+ issue_5.labels << label_2
+ issue_8.labels << label_1 # 2
+ issue_1.labels << label_2 # 3
+ issue_1.labels << label_3
+ issue_3.labels << label_2 # 4
+ issue_3.labels << label_4
+ issue_7.labels << label_2 # 5
+ issue_2.labels << label_3 # 6
+ issue_4.labels << label_4 # 7
+ issue_6.labels << label_5 # 8 - No priority
+
+ login_as user
+ visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
+
+ expect(page).to have_selector('.dropdown-toggle', text: 'Priority')
+
+ page.within('.issues-holder') do
+ issue_titles = all('.issues-list .issue-title-text').map(&:text)
+
+ expect(issue_titles[0..1]).to contain_exactly('issue_5', 'issue_8')
+ expect(issue_titles[2..4]).to contain_exactly('issue_1', 'issue_3', 'issue_7')
+ expect(issue_titles[5..-1]).to eq(['issue_2', 'issue_4', 'issue_6'])
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
new file mode 100644
index 00000000000..8550d279d09
--- /dev/null
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+feature 'Prioritize labels', feature: true do
+ include WaitForAjax
+
+ context 'when project belongs to user' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'test', namespace: user.namespace) }
+
+ scenario 'user can prioritize a label', js: true do
+ bug = create(:label, title: 'bug')
+ wontfix = create(:label, title: 'wontfix')
+
+ project.labels << bug
+ project.labels << wontfix
+
+ login_as user
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content('No prioritized labels yet')
+
+ page.within('.other-labels') do
+ first('.js-toggle-priority').click
+ wait_for_ajax
+ expect(page).not_to have_content('bug')
+ end
+
+ page.within('.prioritized-labels') do
+ expect(page).not_to have_content('No prioritized labels yet')
+ expect(page).to have_content('bug')
+ end
+ end
+
+ scenario 'user can unprioritize a label', js: true do
+ bug = create(:label, title: 'bug', priority: 1)
+ wontfix = create(:label, title: 'wontfix')
+
+ project.labels << bug
+ project.labels << wontfix
+
+ login_as user
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content('bug')
+
+ page.within('.prioritized-labels') do
+ first('.js-toggle-priority').click
+ wait_for_ajax
+ expect(page).not_to have_content('bug')
+ end
+
+ page.within('.other-labels') do
+ expect(page).to have_content('bug')
+ expect(page).to have_content('wontfix')
+ end
+ end
+
+ scenario 'user can sort prioritized labels and persist across reloads', js: true do
+ bug = create(:label, title: 'bug', priority: 1)
+ wontfix = create(:label, title: 'wontfix', priority: 2)
+
+ project.labels << bug
+ project.labels << wontfix
+
+ login_as user
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+
+ # Sort labels
+ find("#label_#{bug.id}").drag_to find("#label_#{wontfix.id}")
+
+ page.within('.prioritized-labels') do
+ expect(first('li')).to have_content('wontfix')
+ expect(page.all('li').last).to have_content('bug')
+ end
+
+ visit current_url
+
+ page.within('.prioritized-labels') do
+ expect(first('li')).to have_content('wontfix')
+ expect(page.all('li').last).to have_content('bug')
+ end
+ end
+ end
+
+ context 'as a guest' do
+ it 'can not prioritize labels' do
+ user = create(:user)
+ guest = create(:user)
+ project = create(:project, name: 'test', namespace: user.namespace)
+
+ create(:label, title: 'bug')
+
+ login_as guest
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).not_to have_css('.prioritized-labels')
+ end
+ end
+
+ context 'as a non signed in user' do
+ it 'can not prioritize labels' do
+ user = create(:user)
+ project = create(:project, name: 'test', namespace: user.namespace)
+
+ create(:label, title: 'bug')
+
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).not_to have_css('.prioritized-labels')
+ end
+ end
+end
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
new file mode 100644
index 00000000000..5fe4caa12f0
--- /dev/null
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Master manages access requests', feature: true do
+ let(:user) { create(:user) }
+ let(:master) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ background do
+ project.request_access(user)
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ scenario 'master can see access requests' do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ expect_visible_access_request(project, user)
+ end
+
+ scenario 'master can grant access' do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ expect_visible_access_request(project, user)
+
+ perform_enqueued_jobs { click_on 'Grant access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was granted"
+ end
+
+ scenario 'master can deny access' do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ expect_visible_access_request(project, user)
+
+ perform_enqueued_jobs { click_on 'Deny access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was denied"
+ end
+
+ def expect_visible_access_request(project, user)
+ expect(project.members.request.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content "#{project.name} access requests (1)"
+ expect(page).to have_content user.name
+ end
+end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
new file mode 100644
index 00000000000..fd92a3a2f0c
--- /dev/null
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+feature 'Projects > Members > User requests access', feature: true do
+ let(:user) { create(:user) }
+ let(:master) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ background do
+ project.team << [master, :master]
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user can request access to a project' do
+ perform_enqueued_jobs { click_link 'Request Access' }
+
+ expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email]
+ expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.name_with_namespace} project"
+
+ expect(project.members.request.exists?(user_id: user)).to be_truthy
+ expect(page).to have_content 'Your request for access has been queued for review.'
+
+ expect(page).to have_content 'Withdraw Access Request'
+ end
+
+ scenario 'user is not listed in the project members page' do
+ click_link 'Request Access'
+
+ expect(project.members.request.exists?(user_id: user)).to be_truthy
+
+ open_project_settings_menu
+ click_link 'Members'
+
+ visit namespace_project_project_members_path(project.namespace, project)
+ page.within('.content') do
+ expect(page).not_to have_content(user.name)
+ end
+ end
+
+ scenario 'user can withdraw its request for access' do
+ click_link 'Request Access'
+
+ expect(project.members.request.exists?(user_id: user)).to be_truthy
+
+ click_link 'Withdraw Access Request'
+
+ expect(project.members.request.exists?(user_id: user)).to be_falsey
+ expect(page).to have_content 'Your access request to the project has been withdrawn.'
+ end
+
+ def open_project_settings_menu
+ find('#project-settings-button').click
+ end
+end
diff --git a/spec/features/project/shortcuts_spec.rb b/spec/features/projects/shortcuts_spec.rb
index 2595c4181e5..54aa9c66a08 100644
--- a/spec/features/project/shortcuts_spec.rb
+++ b/spec/features/projects/shortcuts_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
feature 'Project shortcuts', feature: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, name: 'Victorialand') }
let(:user) { create(:user) }
describe 'On a project', js: true do
@@ -14,7 +14,7 @@ feature 'Project shortcuts', feature: true do
describe 'pressing "i"' do
it 'redirects to new issue page' do
find('body').native.send_key('i')
- expect(page).to have_content('New Issue')
+ expect(page).to have_content('Victorialand')
end
end
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index 8edeb8d18af..a5ed3595b0a 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -29,8 +29,8 @@ describe "Runners" do
end
before do
- expect(page).to_not have_content(@specific_runner3.display_name)
- expect(page).to_not have_content(@specific_runner3.display_name)
+ expect(page).not_to have_content(@specific_runner3.display_name)
+ expect(page).not_to have_content(@specific_runner3.display_name)
end
it "places runners in right places" do
@@ -110,4 +110,37 @@ describe "Runners" do
expect(page).to have_content(@specific_runner.platform)
end
end
+
+ feature 'configuring runners ability to picking untagged jobs' do
+ given(:project) { create(:empty_project) }
+ given(:runner) { create(:ci_runner) }
+
+ background do
+ project.team << [user, :master]
+ project.runners << runner
+ end
+
+ scenario 'user checks default configuration' do
+ visit namespace_project_runner_path(project.namespace, project, runner)
+
+ expect(page).to have_content 'Can run untagged jobs Yes'
+ end
+
+ context 'when runner has tags' do
+ before { runner.update_attribute(:tag_list, ['tag']) }
+
+ scenario 'user wants to prevent runner from running untagged job' do
+ visit runners_path(project)
+ page.within('.activated-specific-runners') do
+ first('small > a').click
+ end
+
+ uncheck 'runner_run_untagged'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Can run untagged jobs No'
+ expect(runner.reload.run_untagged?).to eq false
+ end
+ end
+ end
end
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 4def4f99bc0..c5f741709ad 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -142,8 +142,8 @@ describe "Public Project Access", feature: true do
end
describe "GET /:project_path/builds/:id" do
- let(:commit) { create(:ci_commit, project: project) }
- let(:build) { create(:ci_build, commit: commit) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
subject { namespace_project_build_path(project.namespace, project, build.id) }
context "when allowed for public" do
diff --git a/spec/features/tags/master_updates_tag_spec.rb b/spec/features/tags/master_updates_tag_spec.rb
index c926e9841f3..6b5b3122f72 100644
--- a/spec/features/tags/master_updates_tag_spec.rb
+++ b/spec/features/tags/master_updates_tag_spec.rb
@@ -12,7 +12,7 @@ feature 'Master updates tag', feature: true do
context 'from the tags list page' do
scenario 'updates the release notes' do
- page.within(first('.controls')) do
+ page.within(first('.content-list .controls')) do
click_link 'Edit release notes'
end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index b7368cca29d..6ed279ef9be 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -75,7 +75,10 @@ feature 'Task Lists', feature: true do
describe 'for Notes' do
let!(:issue) { create(:issue, author: user, project: project) }
- let!(:note) { create(:note, note: markdown, noteable: issue, author: user) }
+ let!(:note) do
+ create(:note, note: markdown, noteable: issue,
+ project: project, author: user)
+ end
it 'renders for note body' do
visit_issue(project, issue)
diff --git a/spec/features/todos/target_state_spec.rb b/spec/features/todos/target_state_spec.rb
new file mode 100644
index 00000000000..32fa88a2b21
--- /dev/null
+++ b/spec/features/todos/target_state_spec.rb
@@ -0,0 +1,65 @@
+require 'rails_helper'
+
+feature 'Todo target states', feature: true do
+ let(:user) { create(:user) }
+ let(:author) { create(:user) }
+ let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+
+ before do
+ login_as user
+ end
+
+ scenario 'on a closed issue todo has closed label' do
+ issue_closed = create(:issue, state: 'closed')
+ create_todo issue_closed
+ visit dashboard_todos_path
+
+ page.within '.todos-list' do
+ expect(page).to have_content('Closed')
+ end
+ end
+
+ scenario 'on an open issue todo does not have an open label' do
+ issue_open = create(:issue)
+ create_todo issue_open
+ visit dashboard_todos_path
+
+ page.within '.todos-list' do
+ expect(page).not_to have_content('Open')
+ end
+ end
+
+ scenario 'on a merged merge request todo has merged label' do
+ mr_merged = create(:merge_request, :simple, author: user, state: 'merged')
+ create_todo mr_merged
+ visit dashboard_todos_path
+
+ page.within '.todos-list' do
+ expect(page).to have_content('Merged')
+ end
+ end
+
+ scenario 'on a closed merge request todo has closed label' do
+ mr_closed = create(:merge_request, :simple, author: user, state: 'closed')
+ create_todo mr_closed
+ visit dashboard_todos_path
+
+ page.within '.todos-list' do
+ expect(page).to have_content('Closed')
+ end
+ end
+
+ scenario 'on an open merge request todo does not have an open label' do
+ mr_open = create(:merge_request, :simple, author: user)
+ create_todo mr_open
+ visit dashboard_todos_path
+
+ page.within '.todos-list' do
+ expect(page).not_to have_content('Open')
+ end
+ end
+
+ def create_todo(target)
+ create(:todo, :mentioned, user: user, project: project, target: target, author: author)
+ end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 3354f529295..8e1833a069e 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'Dashboard Todos', feature: true do
let(:user) { create(:user) }
let(:author) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:issue) { create(:issue) }
describe 'GET /dashboard/todos' do
@@ -43,6 +43,27 @@ describe 'Dashboard Todos', feature: true do
end
end
+ context 'User has Todos with labels spanning multiple projects' do
+ before do
+ label1 = create(:label, project: project)
+ note1 = create(:note_on_issue, note: "Hello #{label1.to_reference(format: :name)}", noteable_id: issue.id, noteable_type: 'Issue', project: issue.project)
+ create(:todo, :mentioned, project: project, target: issue, user: user, note_id: note1.id)
+
+ project2 = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ label2 = create(:label, project: project2)
+ issue2 = create(:issue, project: project2)
+ note2 = create(:note_on_issue, note: "Test #{label2.to_reference(format: :name)}", noteable_id: issue2.id, noteable_type: 'Issue', project: project2)
+ create(:todo, :mentioned, project: project2, target: issue2, user: user, note_id: note2.id)
+
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows page with two Todos' do
+ expect(page).to have_selector('.todos-list .todo', count: 2)
+ end
+ end
+
context 'User has multiple pages of Todos' do
before do
allow(Todo).to receive(:default_per_page).and_return(1)
@@ -77,5 +98,18 @@ describe 'Dashboard Todos', feature: true do
end
end
end
+
+ context 'User has a Todo in a project pending deletion' do
+ before do
+ deleted_project = create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC, pending_delete: true)
+ create(:todo, :mentioned, user: user, project: deleted_project, target: issue, author: author)
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it 'shows "All done" message' do
+ expect(page).to have_content "You're all done!"
+ end
+ end
end
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
new file mode 100644
index 00000000000..366a90228b1
--- /dev/null
+++ b/spec/features/u2f_spec.rb
@@ -0,0 +1,239 @@
+require 'spec_helper'
+
+feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do
+ def register_u2f_device(u2f_device = nil)
+ u2f_device ||= FakeU2fDevice.new(page)
+ u2f_device.respond_to_u2f_registration
+ click_on 'Setup New U2F Device'
+ expect(page).to have_content('Your device was successfully set up')
+ click_on 'Register U2F Device'
+ u2f_device
+ end
+
+ describe "registration" do
+ let(:user) { create(:user) }
+ before { login_as(user) }
+
+ describe 'when 2FA via OTP is disabled' do
+ it 'allows registering a new device' do
+ visit profile_account_path
+ click_on 'Enable Two-Factor Authentication'
+
+ register_u2f_device
+
+ expect(page.body).to match('Your U2F device was registered')
+ end
+
+ it 'allows registering more than one device' do
+ visit profile_account_path
+
+ # First device
+ click_on 'Enable Two-Factor Authentication'
+ register_u2f_device
+ expect(page.body).to match('Your U2F device was registered')
+
+ # Second device
+ click_on 'Manage Two-Factor Authentication'
+ register_u2f_device
+ expect(page.body).to match('Your U2F device was registered')
+ click_on 'Manage Two-Factor Authentication'
+
+ expect(page.body).to match('You have 2 U2F devices registered')
+ end
+ end
+
+ describe 'when 2FA via OTP is enabled' do
+ before { user.update_attributes(otp_required_for_login: true) }
+
+ it 'allows registering a new device' do
+ visit profile_account_path
+ click_on 'Manage Two-Factor Authentication'
+ expect(page.body).to match("You've already enabled two-factor authentication using mobile")
+
+ register_u2f_device
+
+ expect(page.body).to match('Your U2F device was registered')
+ end
+
+ it 'allows registering more than one device' do
+ visit profile_account_path
+
+ # First device
+ click_on 'Manage Two-Factor Authentication'
+ register_u2f_device
+ expect(page.body).to match('Your U2F device was registered')
+
+ # Second device
+ click_on 'Manage Two-Factor Authentication'
+ register_u2f_device
+ expect(page.body).to match('Your U2F device was registered')
+
+ click_on 'Manage Two-Factor Authentication'
+ expect(page.body).to match('You have 2 U2F devices registered')
+ end
+ end
+
+ it 'allows the same device to be registered for multiple users' do
+ # First user
+ visit profile_account_path
+ click_on 'Enable Two-Factor Authentication'
+ u2f_device = register_u2f_device
+ expect(page.body).to match('Your U2F device was registered')
+ logout
+
+ # Second user
+ login_as(:user)
+ visit profile_account_path
+ click_on 'Enable Two-Factor Authentication'
+ register_u2f_device(u2f_device)
+ expect(page.body).to match('Your U2F device was registered')
+
+ expect(U2fRegistration.count).to eq(2)
+ end
+
+ context "when there are form errors" do
+ it "doesn't register the device if there are errors" do
+ visit profile_account_path
+ click_on 'Enable Two-Factor Authentication'
+
+ # Have the "u2f device" respond with bad data
+ page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
+ click_on 'Setup New U2F Device'
+ expect(page).to have_content('Your device was successfully set up')
+ click_on 'Register U2F Device'
+
+ expect(U2fRegistration.count).to eq(0)
+ expect(page.body).to match("The form contains the following error")
+ expect(page.body).to match("did not send a valid JSON response")
+ end
+
+ it "allows retrying registration" do
+ visit profile_account_path
+ click_on 'Enable Two-Factor Authentication'
+
+ # Failed registration
+ page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
+ click_on 'Setup New U2F Device'
+ expect(page).to have_content('Your device was successfully set up')
+ click_on 'Register U2F Device'
+ expect(page.body).to match("The form contains the following error")
+
+ # Successful registration
+ register_u2f_device
+
+ expect(page.body).to match('Your U2F device was registered')
+ expect(U2fRegistration.count).to eq(1)
+ end
+ end
+ end
+
+ describe "authentication" do
+ let(:user) { create(:user) }
+
+ before do
+ # Register and logout
+ login_as(user)
+ visit profile_account_path
+ click_on 'Enable Two-Factor Authentication'
+ @u2f_device = register_u2f_device
+ logout
+ end
+
+ describe "when 2FA via OTP is disabled" do
+ it "allows logging in with the U2F device" do
+ login_with(user)
+
+ @u2f_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
+
+ expect(page.body).to match('Signed in successfully')
+ end
+ end
+
+ describe "when 2FA via OTP is enabled" do
+ it "allows logging in with the U2F device" do
+ user.update_attributes(otp_required_for_login: true)
+ login_with(user)
+
+ @u2f_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
+
+ expect(page.body).to match('Signed in successfully')
+ end
+ end
+
+ describe "when a given U2F device has already been registered by another user" do
+ describe "but not the current user" do
+ it "does not allow logging in with that particular device" do
+ # Register current user with the different U2F device
+ current_user = login_as(:user)
+ visit profile_account_path
+ click_on 'Enable Two-Factor Authentication'
+ register_u2f_device
+ logout
+
+ # Try authenticating user with the old U2F device
+ login_as(current_user)
+ @u2f_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
+
+ expect(page.body).to match('Authentication via U2F device failed')
+ end
+ end
+
+ describe "and also the current user" do
+ it "allows logging in with that particular device" do
+ # Register current user with the same U2F device
+ current_user = login_as(:user)
+ visit profile_account_path
+ click_on 'Enable Two-Factor Authentication'
+ register_u2f_device(@u2f_device)
+ logout
+
+ # Try authenticating user with the same U2F device
+ login_as(current_user)
+ @u2f_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
+
+ expect(page.body).to match('Signed in successfully')
+ end
+ end
+ end
+
+ describe "when a given U2F device has not been registered" do
+ it "does not allow logging in with that particular device" do
+ unregistered_device = FakeU2fDevice.new(page)
+ login_as(user)
+ unregistered_device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
+
+ expect(page.body).to match('Authentication via U2F device failed')
+ end
+ end
+ end
+
+ describe "when two-factor authentication is disabled" do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ visit profile_account_path
+ click_on 'Enable Two-Factor Authentication'
+ register_u2f_device
+ end
+
+ it "deletes u2f registrations" do
+ expect { click_on "Disable" }.to change { U2fRegistration.count }.from(1).to(0)
+ end
+ end
+end
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index afea1840cd7..a2b8f7b6931 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -1,24 +1,53 @@
require 'spec_helper'
-describe "Variables" do
- let(:user) { create(:user) }
- before { login_as(user) }
-
- describe "specific runners" do
- before do
- @project = FactoryGirl.create :empty_project
- @project.team << [user, :master]
+describe 'Project variables', js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:variable) { create(:ci_variable, key: 'test') }
+
+ before do
+ login_as(user)
+ project.team << [user, :master]
+ project.variables << variable
+
+ visit namespace_project_variables_path(project.namespace, project)
+ end
+
+ it 'should show list of variables' do
+ page.within('.variables-table') do
+ expect(page).to have_content(variable.key)
+ end
+ end
+
+ it 'should add new variable' do
+ fill_in('variable_key', with: 'key')
+ fill_in('variable_value', with: 'key value')
+ click_button('Add new variable')
+
+ page.within('.variables-table') do
+ expect(page).to have_content('key')
+ end
+ end
+
+ it 'should delete variable' do
+ page.within('.variables-table') do
+ find('.btn-variable-delete').click
+ end
+
+ expect(page).not_to have_selector('variables-table')
+ end
+
+ it 'should edit variable' do
+ page.within('.variables-table') do
+ find('.btn-variable-edit').click
end
- it "creates variable", js: true do
- visit namespace_project_variables_path(@project.namespace, @project)
- click_on "Add a variable"
- fill_in "Key", with: "SECRET_KEY"
- fill_in "Value", with: "SECRET_VALUE"
- click_on "Save changes"
+ fill_in('variable_key', with: 'key')
+ fill_in('variable_value', with: 'key value')
+ click_button('Save variable')
- expect(page).to have_content("Variables were successfully updated.")
- expect(@project.variables.count).to eq(1)
+ page.within('.variables-table') do
+ expect(page).to have_content('key')
end
end
end
diff --git a/spec/fixtures/container_registry/config_blob.json b/spec/fixtures/container_registry/config_blob.json
new file mode 100644
index 00000000000..1028c994a24
--- /dev/null
+++ b/spec/fixtures/container_registry/config_blob.json
@@ -0,0 +1 @@
+{"architecture":"amd64","config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"b14cd82987550b01af9a666a2f4c996280a6152e66873134fae5a0f223dc5976","container_config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"],"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2016-04-01T20:53:00.160300546Z","docker_version":"1.9.1","history":[{"created":"2016-04-01T20:53:00.160300546Z","created_by":"/bin/sh -c #(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:c56b7dabbc7aa730eeab07668bdcbd7e3d40855047ca9a0cc1bfed23a2486111"]}}
diff --git a/spec/fixtures/container_registry/tag_manifest.json b/spec/fixtures/container_registry/tag_manifest.json
new file mode 100644
index 00000000000..1b6008e2872
--- /dev/null
+++ b/spec/fixtures/container_registry/tag_manifest.json
@@ -0,0 +1 @@
+{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/octet-stream","size":1145,"digest":"sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2319870,"digest":"sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"}]}
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 1772cc3f6a4..c75d28d9801 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -136,7 +136,7 @@ But it shouldn't autolink text inside certain tags:
### ExternalLinkFilter
-External links get a `rel="nofollow"` attribute:
+External links get a `rel="nofollow noreferrer"` and `target="_blank"` attributes:
- [Google](https://google.com/)
- [GitLab Root](<%= Gitlab.config.gitlab.url %>)
@@ -216,10 +216,14 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
#### MilestoneReferenceFilter
-- Milestone: <%= milestone.to_reference %>
+- Milestone by ID: <%= simple_milestone.to_reference %>
+- Milestone by name: <%= Milestone.reference_prefix %><%= simple_milestone.name %>
+- Milestone by name in quotes: <%= milestone.to_reference(format: :name) %>
- Milestone in another project: <%= xmilestone.to_reference(project) %>
-- Ignored in code: `<%= milestone.to_reference %>`
-- Link to milestone by URL: [Milestone](<%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>)
+- Ignored in code: `<%= simple_milestone.to_reference %>`
+- Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link)
+- Milestone by URL: <%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>
+- Link to milestone by URL: [Milestone](<%= milestone.to_reference %>)
### Task Lists
@@ -239,3 +243,16 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- [[link-text|http://example.com/pdfs/gollum.pdf]]
- [[images/example.jpg]]
- [[http://example.com/images/example.jpg]]
+
+### Inline Diffs
+
+With inline diffs tags you can display {+ additions +} or [- deletions -].
+
+The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
+
+However the wrapping tags can not be mixed as such -
+
+- {+ additions +]
+- [+ additions +}
+- {- delletions -]
+- [- delletions -}
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 16fbb5dcecb..49ea4fa6d3e 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -36,7 +36,7 @@ describe AuthHelper do
)
expect(helper.enabled_button_based_providers).to include('twitter')
- expect(helper.enabled_button_based_providers).to_not include('github')
+ expect(helper.enabled_button_based_providers).not_to include('github')
end
end
end
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index f942695b6f0..45199d0f09d 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe CiStatusHelper do
include IconsHelper
- let(:success_commit) { double("Ci::Commit", status: 'success') }
- let(:failed_commit) { double("Ci::Commit", status: 'failed') }
+ let(:success_commit) { double("Ci::Pipeline", status: 'success') }
+ let(:failed_commit) { double("Ci::Pipeline", status: 'failed') }
describe 'ci_icon_for_status' do
it { expect(helper.ci_icon_for_status(success_commit.status)).to include('fa-check') }
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index b7810185d16..52764f41e0d 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -93,9 +93,9 @@ describe DiffHelper do
it "returns strings with marked inline diffs" do
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
- expect(marked_old_line).to eq("abc <span class='idiff left right'>&#39;def&#39;</span>")
+ expect(marked_old_line).to eq("abc <span class='idiff left right deletion'>&#39;def&#39;</span>")
expect(marked_old_line).to be_html_safe
- expect(marked_new_line).to eq("abc <span class='idiff left right'>&quot;def&quot;</span>")
+ expect(marked_new_line).to eq("abc <span class='idiff left right addition'>&quot;def&quot;</span>")
expect(marked_new_line).to be_html_safe
end
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 13de88e2f21..ade5c3b02d9 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -121,13 +121,14 @@ describe GitlabMarkdownHelper do
before do
@wiki = double('WikiPage')
allow(@wiki).to receive(:content).and_return('wiki content')
+ allow(@wiki).to receive(:slug).and_return('nested/page')
helper.instance_variable_set(:@project_wiki, @wiki)
end
it "should use Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown)
- expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki)
+ expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page")
helper.render_wiki_content(@wiki)
end
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
new file mode 100644
index 00000000000..14847d0a49e
--- /dev/null
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe GitlabRoutingHelper do
+ describe 'Project URL helpers' do
+ describe '#project_members_url' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(project_members_url(project)).to eq namespace_project_project_members_url(project.namespace, project) }
+ end
+
+ describe '#project_member_path' do
+ let(:project_member) { create(:project_member) }
+
+ it { expect(project_member_path(project_member)).to eq namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+ end
+
+ describe '#request_access_project_members_path' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(request_access_project_members_path(project)).to eq request_access_namespace_project_project_members_path(project.namespace, project) }
+ end
+
+ describe '#leave_project_members_path' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(leave_project_members_path(project)).to eq leave_namespace_project_project_members_path(project.namespace, project) }
+ end
+
+ describe '#approve_access_request_project_member_path' do
+ let(:project_member) { create(:project_member) }
+
+ it { expect(approve_access_request_project_member_path(project_member)).to eq approve_access_request_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+ end
+
+ describe '#resend_invite_project_member_path' do
+ let(:project_member) { create(:project_member) }
+
+ it { expect(resend_invite_project_member_path(project_member)).to eq resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member) }
+ end
+ end
+
+ describe 'Group URL helpers' do
+ describe '#group_members_url' do
+ let(:group) { build_stubbed(:group) }
+
+ it { expect(group_members_url(group)).to eq group_group_members_url(group) }
+ end
+
+ describe '#group_member_path' do
+ let(:group_member) { create(:group_member) }
+
+ it { expect(group_member_path(group_member)).to eq group_group_member_path(group_member.source, group_member) }
+ end
+
+ describe '#request_access_group_members_path' do
+ let(:group) { build_stubbed(:group) }
+
+ it { expect(request_access_group_members_path(group)).to eq request_access_group_group_members_path(group) }
+ end
+
+ describe '#leave_group_members_path' do
+ let(:group) { build_stubbed(:group) }
+
+ it { expect(leave_group_members_path(group)).to eq leave_group_group_members_path(group) }
+ end
+
+ describe '#approve_access_request_group_member_path' do
+ let(:group_member) { create(:group_member) }
+
+ it { expect(approve_access_request_group_member_path(group_member)).to eq approve_access_request_group_group_member_path(group_member.source, group_member) }
+ end
+
+ describe '#resend_invite_group_member_path' do
+ let(:group_member) { create(:group_member) }
+
+ it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
+ end
+ end
+end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index bffe2c18b6f..831ae7fb69c 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -7,10 +7,7 @@ describe IssuesHelper do
describe "url_for_project_issues" do
let(:project_url) { ext_project.external_issue_tracker.project_url }
- let(:ext_expected) do
- project_url.gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { project_url.gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { polymorphic_path([@project.namespace, project]) }
it "should return internal path if used internal tracker" do
@@ -56,11 +53,7 @@ describe IssuesHelper do
describe "url_for_issue" do
let(:issues_url) { ext_project.external_issue_tracker.issues_url}
- let(:ext_expected) do
- issues_url.gsub(':id', issue.iid.to_s)
- .gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) }
it "should return internal path if used internal tracker" do
@@ -106,10 +99,7 @@ describe IssuesHelper do
describe 'url_for_new_issue' do
let(:issues_url) { ext_project.external_issue_tracker.new_issue_url }
- let(:ext_expected) do
- issues_url.gsub(':project_id', ext_project.id.to_s)
- .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s)
- end
+ let(:ext_expected) { issues_url.gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { new_namespace_project_issue_path(project.namespace, project) }
it "should return internal path if used internal tracker" do
@@ -163,18 +153,15 @@ describe IssuesHelper do
it { is_expected.to eq("!1, !2, or !3") }
end
- describe "note_active_class" do
- before do
- @note = create :note
- @note1 = create :note
- end
+ describe '#award_active_class' do
+ let!(:upvote) { create(:award_emoji) }
it "returns empty string for unauthenticated user" do
- expect(note_active_class(Note.all, nil)).to eq("")
+ expect(award_active_class(AwardEmoji.all, nil)).to eq("")
end
it "returns active string for author" do
- expect(note_active_class(Note.all, @note.author)).to eq("active")
+ expect(award_active_class(AwardEmoji.all, upvote.user)).to eq("active")
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
new file mode 100644
index 00000000000..0b1a76156e0
--- /dev/null
+++ b/spec/helpers/members_helper_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe MembersHelper do
+ describe '#action_member_permission' do
+ let(:project_member) { build(:project_member) }
+ let(:group_member) { build(:group_member) }
+
+ it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member }
+ it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
+ end
+
+ describe '#can_see_member_roles?' do
+ let(:project) { create(:empty_project) }
+ let(:group) { create(:group) }
+ let(:user) { build(:user) }
+ let(:admin) { build(:user, :admin) }
+ let(:project_member) { create(:project_member, project: project) }
+ let(:group_member) { create(:group_member, group: group) }
+
+ it { expect(can_see_member_roles?(source: project, user: nil)).to be_falsy }
+ it { expect(can_see_member_roles?(source: group, user: nil)).to be_falsy }
+ it { expect(can_see_member_roles?(source: project, user: admin)).to be_truthy }
+ it { expect(can_see_member_roles?(source: group, user: admin)).to be_truthy }
+ it { expect(can_see_member_roles?(source: project, user: project_member.user)).to be_truthy }
+ it { expect(can_see_member_roles?(source: group, user: group_member.user)).to be_truthy }
+ end
+
+ describe '#remove_member_message' do
+ let(:requester) { build(:user) }
+ let(:project) { create(:project) }
+ let(:project_member) { build(:project_member, project: project) }
+ let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
+ let(:project_member_request) { project.request_access(requester) }
+ let(:group) { create(:group) }
+ let(:group_member) { build(:group_member, group: group) }
+ let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
+ let(:group_member_request) { group.request_access(requester) }
+
+ it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" }
+ it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" }
+ it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" }
+ it { expect(remove_member_message(group_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{group.name} group?" }
+ end
+
+ describe '#remove_member_title' do
+ let(:requester) { build(:user) }
+ let(:project) { create(:project) }
+ let(:project_member) { build(:project_member, project: project) }
+ let(:project_member_request) { project.request_access(requester) }
+ let(:group) { create(:group) }
+ let(:group_member) { build(:group_member, group: group) }
+ let(:group_member_request) { group.request_access(requester) }
+
+ it { expect(remove_member_title(project_member)).to eq 'Remove user from project' }
+ it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' }
+ it { expect(remove_member_title(group_member)).to eq 'Remove user from group' }
+ it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' }
+ end
+
+ describe '#leave_confirmation_message' do
+ let(:project) { build_stubbed(:project) }
+ let(:group) { build_stubbed(:group) }
+ let(:user) { build_stubbed(:user) }
+
+ it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.name_with_namespace}\" project?" }
+ it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" }
+ end
+end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 600e1c4e9ec..a3336c87173 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -5,7 +5,7 @@ describe MergeRequestsHelper do
let(:project) { create :project }
let(:merge_request) { MergeRequest.new }
let(:ci_service) { CiService.new }
- let(:last_commit) { Ci::Commit.new({}) }
+ let(:last_commit) { Ci::Pipeline.new({}) }
before do
allow(merge_request).to receive(:source_project).and_return(project)
@@ -17,7 +17,7 @@ describe MergeRequestsHelper do
it 'does not include api credentials in a link' do
allow(ci_service).
to receive(:build_page).and_return("http://secretuser:secretpass@jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c")
- expect(helper.ci_build_details_path(merge_request)).to_not match("secret")
+ expect(helper.ci_build_details_path(merge_request)).not_to match("secret")
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index ac5af8740dc..09e0bbfd00b 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -45,16 +45,6 @@ describe ProjectsHelper do
end
end
- describe 'user_max_access_in_project' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- before do
- project.team.add_user(user, Gitlab::Access::MASTER)
- end
-
- it { expect(helper.user_max_access_in_project(user.id, project)).to eq('Master') }
- end
-
describe "readme_cache_key" do
let(:project) { create(:project) }
diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee
new file mode 100644
index 00000000000..ba191199dc7
--- /dev/null
+++ b/spec/javascripts/awards_handler_spec.js.coffee
@@ -0,0 +1,201 @@
+#= require awards_handler
+#= require jquery
+#= require jquery.cookie
+#= require ./fixtures/emoji_menu
+
+awardsHandler = null
+window.gl or= {}
+window.gon or= {}
+gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' }
+gon.award_menu_url = '/emojis'
+
+
+lazyAssert = (done, assertFn) ->
+
+ setTimeout -> # Maybe jasmine.clock here?
+ assertFn()
+ done()
+ , 333
+
+
+describe 'AwardsHandler', ->
+
+ fixture.preload 'awards_handler.html'
+
+ beforeEach ->
+ fixture.load 'awards_handler.html'
+ awardsHandler = new AwardsHandler
+ spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb()
+ spyOn(jQuery, 'get').and.callFake (req, cb) -> cb window.emojiMenu
+
+
+ describe '::showEmojiMenu', ->
+
+ it 'should show emoji menu when Add emoji button clicked', (done) ->
+
+ $('.js-add-award').eq(0).click()
+
+ lazyAssert done, ->
+ $emojiMenu = $ '.emoji-menu'
+ expect($emojiMenu.length).toBe 1
+ expect($emojiMenu.hasClass('is-visible')).toBe yes
+ expect($emojiMenu.find('#emoji_search').length).toBe 1
+ expect($('.js-awards-block.current').length).toBe 1
+
+
+ it 'should also show emoji menu for the smiley icon in notes', (done) ->
+
+ $('.note-action-button').click()
+
+ lazyAssert done, ->
+ $emojiMenu = $ '.emoji-menu'
+ expect($emojiMenu.length).toBe 1
+
+
+ it 'should remove emoji menu when body is clicked', (done) ->
+
+ $('.js-add-award').eq(0).click()
+
+ lazyAssert done, ->
+ $emojiMenu = $('.emoji-menu')
+ $('body').click()
+ expect($emojiMenu.length).toBe 1
+ expect($emojiMenu.hasClass('is-visible')).toBe no
+ expect($('.js-awards-block.current').length).toBe 0
+
+
+ describe '::addAwardToEmojiBar', ->
+
+ it 'should add emoji to votes block', ->
+
+ $votesBlock = $('.js-awards-block').eq 0
+ awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
+
+ $emojiButton = $votesBlock.find '[data-emoji=heart]'
+
+ expect($emojiButton.length).toBe 1
+ expect($emojiButton.next('.js-counter').text()).toBe '1'
+ expect($votesBlock.hasClass('hidden')).toBe no
+
+
+ it 'should remove the emoji when we click again', ->
+
+ $votesBlock = $('.js-awards-block').eq 0
+ awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
+ awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
+ $emojiButton = $votesBlock.find '[data-emoji=heart]'
+
+ expect($emojiButton.length).toBe 0
+
+
+ it 'should decrement the emoji counter', ->
+
+ $votesBlock = $('.js-awards-block').eq 0
+ awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
+
+ $emojiButton = $votesBlock.find '[data-emoji=heart]'
+ $emojiButton.next('.js-counter').text 5
+
+ awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
+
+ expect($emojiButton.length).toBe 1
+ expect($emojiButton.next('.js-counter').text()).toBe '4'
+
+
+ describe '::getAwardUrl', ->
+
+ it 'should return the url for request', ->
+
+ expect(awardsHandler.getAwardUrl()).toBe '/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'
+
+
+ describe '::addAward and ::checkMutuality', ->
+
+ it 'should handle :+1: and :-1: mutuality', ->
+
+ awardUrl = awardsHandler.getAwardUrl()
+ $votesBlock = $('.js-awards-block').eq 0
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent()
+ $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent()
+
+ awardsHandler.addAward $votesBlock, awardUrl, 'thumbsup', no
+
+ expect($thumbsUpEmoji.hasClass('active')).toBe yes
+ expect($thumbsDownEmoji.hasClass('active')).toBe no
+
+ $thumbsUpEmoji.tooltip()
+ $thumbsDownEmoji.tooltip()
+
+ awardsHandler.addAward $votesBlock, awardUrl, 'thumbsdown', yes
+
+ expect($thumbsUpEmoji.hasClass('active')).toBe no
+ expect($thumbsDownEmoji.hasClass('active')).toBe yes
+
+
+ describe '::removeEmoji', ->
+
+ it 'should remove emoji', ->
+
+ awardUrl = awardsHandler.getAwardUrl()
+ $votesBlock = $('.js-awards-block').eq 0
+
+ awardsHandler.addAward $votesBlock, awardUrl, 'fire', no
+ expect($votesBlock.find('[data-emoji=fire]').length).toBe 1
+
+ awardsHandler.removeEmoji $votesBlock.find('[data-emoji=fire]').closest('button')
+ expect($votesBlock.find('[data-emoji=fire]').length).toBe 0
+
+
+ describe 'search', ->
+
+ it 'should filter the emoji', ->
+
+ $('.js-add-award').eq(0).click()
+
+ expect($('[data-emoji=angel]').is(':visible')).toBe yes
+ expect($('[data-emoji=anger]').is(':visible')).toBe yes
+
+ $('#emoji_search').val('ali').trigger 'keyup'
+
+ expect($('[data-emoji=angel]').is(':visible')).toBe no
+ expect($('[data-emoji=anger]').is(':visible')).toBe no
+ expect($('[data-emoji=alien]').is(':visible')).toBe yes
+ expect($('h5.emoji-search').is(':visible')).toBe yes
+
+
+ describe 'emoji menu', ->
+
+ selector = '[data-emoji=sunglasses]'
+
+ openEmojiMenuAndAddEmoji = ->
+
+ $('.js-add-award').eq(0).click()
+
+ $menu = $ '.emoji-menu'
+ $block = $ '.js-awards-block'
+ $emoji = $menu.find ".emoji-menu-list-item #{selector}"
+
+ expect($emoji.length).toBe 1
+ expect($block.find(selector).length).toBe 0
+
+ $emoji.click()
+
+ expect($menu.hasClass('.is-visible')).toBe no
+ expect($block.find(selector).length).toBe 1
+
+
+ it 'should add selected emoji to awards block', ->
+
+ openEmojiMenuAndAddEmoji()
+
+
+ it 'should remove already selected emoji', ->
+
+ openEmojiMenuAndAddEmoji()
+ $('.js-add-award').eq(0).click()
+
+ $block = $ '.js-awards-block'
+ $emoji = $('.emoji-menu').find ".emoji-menu-list-item #{selector}"
+
+ $emoji.click()
+ expect($block.find(selector).length).toBe 0
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee
index 09708c12ed4..d3b003a328a 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee
+++ b/spec/javascripts/behaviors/quick_submit_spec.js.coffee
@@ -14,17 +14,17 @@ describe 'Quick Submit behavior', ->
}
it 'does not respond to other keyCodes', ->
- $('input').trigger(keydownEvent(keyCode: 32))
+ $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32))
expect(@spies.submit).not.toHaveBeenTriggered()
it 'does not respond to Enter alone', ->
- $('input').trigger(keydownEvent(ctrlKey: false, metaKey: false))
+ $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false))
expect(@spies.submit).not.toHaveBeenTriggered()
it 'does not respond to repeated events', ->
- $('input').trigger(keydownEvent(repeat: true))
+ $('input.quick-submit-input').trigger(keydownEvent(repeat: true))
expect(@spies.submit).not.toHaveBeenTriggered()
@@ -38,26 +38,26 @@ describe 'Quick Submit behavior', ->
# only run the tests that apply to the current platform
if navigator.userAgent.match(/Macintosh/)
it 'responds to Meta+Enter', ->
- $('input').trigger(keydownEvent())
+ $('input.quick-submit-input').trigger(keydownEvent())
expect(@spies.submit).toHaveBeenTriggered()
it 'excludes other modifier keys', ->
- $('input').trigger(keydownEvent(altKey: true))
- $('input').trigger(keydownEvent(ctrlKey: true))
- $('input').trigger(keydownEvent(shiftKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(altKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true))
expect(@spies.submit).not.toHaveBeenTriggered()
else
it 'responds to Ctrl+Enter', ->
- $('input').trigger(keydownEvent())
+ $('input.quick-submit-input').trigger(keydownEvent())
expect(@spies.submit).toHaveBeenTriggered()
it 'excludes other modifier keys', ->
- $('input').trigger(keydownEvent(altKey: true))
- $('input').trigger(keydownEvent(metaKey: true))
- $('input').trigger(keydownEvent(shiftKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(altKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(metaKey: true))
+ $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true))
expect(@spies.submit).not.toHaveBeenTriggered()
diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml
new file mode 100644
index 00000000000..d55936ee4f9
--- /dev/null
+++ b/spec/javascripts/fixtures/awards_handler.html.haml
@@ -0,0 +1,52 @@
+.issue-details.issuable-details
+ .detail-page-description.content-block
+ %h2.title Quibusdam sint officiis earum molestiae ipsa autem voluptatem nisi rem.
+ .description.js-task-list-container.is-task-list-enabled
+ .wiki
+ %p Qui exercitationem magnam optio quae fuga earum odio.
+ %textarea.hidden.js-task-list-field Qui exercitationem magnam optio quae fuga earum odio.
+ %small.edited-text
+ .content-block.content-block-small
+ .awards.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/issues/8/toggle_award_emoji"}
+ %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
+ .icon.emoji-icon.emoji-1F44D{"data-aliases" => "", "data-emoji" => "thumbsup", "data-unicode-name" => "1F44D", :title => "thumbsup"}
+ %span.award-control-text.js-counter 0
+ %button.award-control.btn.js-emoji-btn{"data-placement" => "bottom", "data-title" => "", :type => "button"}
+ .icon.emoji-icon.emoji-1F44E{"data-aliases" => "", "data-emoji" => "thumbsdown", "data-unicode-name" => "1F44E", :title => "thumbsdown"}
+ %span.award-control-text.js-counter 0
+ .award-menu-holder.js-award-holder
+ %button.btn.award-control.js-add-award{:type => "button"}
+ %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
+ %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
+ %span.award-control-text Add
+ %section.issuable-discussion
+ #notes
+ %ul#notes-list.notes.main-notes-list.timeline
+ %li#note_348.note.note-row-348.timeline-entry{"data-author-id" => "18", "data-editable" => ""}
+ .timeline-entry-inner
+ .timeline-icon
+ %a{:href => "/u/agustin"}
+ %img.avatar.s40{:alt => "", :src => "#"}/
+ .timeline-content
+ .note-header
+ %a.author_link{:href => "/u/agustin"}
+ %span.author Brenna Stokes
+ .inline.note-headline-light
+ @agustin commented
+ %a{:href => "#note_348"}
+ %time 11 days ago
+ .note-actions
+ %span.note-role Reporter
+ %a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
+ %i.fa.fa-spinner.fa-spin
+ %i.fa.fa-smile-o
+ .js-task-list-container.note-body.is-task-list-enabled
+ .note-text
+ %p Suscipit sunt quia quisquam sed eveniet ipsam.
+ .note-awards
+ .awards.hidden.js-awards-block{"data-award-url" => "/gitlab-org/gitlab-test/notes/348/toggle_award_emoji"}
+ .award-menu-holder.js-award-holder
+ %button.btn.award-control.js-add-award{:type => "button"}
+ %i.fa.fa-smile-o.award-control-icon.award-control-icon-normal
+ %i.fa.fa-spinner.fa-spin.award-control-icon.award-control-icon-loading
+ %span.award-control-text Add
diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
index e3788bee813..dc2ceed42f4 100644
--- a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
+++ b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml
@@ -1,5 +1,5 @@
%form.js-quick-submit{ action: '/foo' }
- %input{ type: 'text' }
+ %input{ type: 'text', class: 'quick-submit-input'}
%textarea
%input{ type: 'submit'} Submit
diff --git a/spec/javascripts/fixtures/emoji_menu.coffee b/spec/javascripts/fixtures/emoji_menu.coffee
new file mode 100644
index 00000000000..e529dd5f1cd
--- /dev/null
+++ b/spec/javascripts/fixtures/emoji_menu.coffee
@@ -0,0 +1,957 @@
+window.emojiMenu = """
+ <div class='emoji-menu'>
+ <div class='emoji-menu-content'>
+ <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" />
+ <h5 class='emoji-menu-title'>
+ Emoticons
+ </h5>
+ <ul class='clearfix emoji-menu-list'>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F47D" title="alien" data-aliases="" data-emoji="alien" data-unicode-name="1F47D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F47C" title="angel" data-aliases="" data-emoji="angel" data-unicode-name="1F47C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A2" title="anger" data-aliases="" data-emoji="anger" data-unicode-name="1F4A2"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F620" title="angry" data-aliases="" data-emoji="angry" data-unicode-name="1F620"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F627" title="anguished" data-aliases="" data-emoji="anguished" data-unicode-name="1F627"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F632" title="astonished" data-aliases="" data-emoji="astonished" data-unicode-name="1F632"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45F" title="athletic_shoe" data-aliases="" data-emoji="athletic_shoe" data-unicode-name="1F45F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F476" title="baby" data-aliases="" data-emoji="baby" data-unicode-name="1F476"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F459" title="bikini" data-aliases="" data-emoji="bikini" data-unicode-name="1F459"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F499" title="blue_heart" data-aliases="" data-emoji="blue_heart" data-unicode-name="1F499"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60A" title="blush" data-aliases="" data-emoji="blush" data-unicode-name="1F60A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A5" title="boom" data-aliases="" data-emoji="boom" data-unicode-name="1F4A5"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F462" title="boot" data-aliases="" data-emoji="boot" data-unicode-name="1F462"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F647" title="bow" data-aliases="" data-emoji="bow" data-unicode-name="1F647"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F466" title="boy" data-aliases="" data-emoji="boy" data-unicode-name="1F466"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F470" title="bride_with_veil" data-aliases="" data-emoji="bride_with_veil" data-unicode-name="1F470"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4BC" title="briefcase" data-aliases="" data-emoji="briefcase" data-unicode-name="1F4BC"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F494" title="broken_heart" data-aliases="" data-emoji="broken_heart" data-unicode-name="1F494"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F464" title="bust_in_silhouette" data-aliases="" data-emoji="bust_in_silhouette" data-unicode-name="1F464"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F465" title="busts_in_silhouette" data-aliases="" data-emoji="busts_in_silhouette" data-unicode-name="1F465"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44F" title="clap" data-aliases="" data-emoji="clap" data-unicode-name="1F44F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F302" title="closed_umbrella" data-aliases="" data-emoji="closed_umbrella" data-unicode-name="1F302"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F630" title="cold_sweat" data-aliases="" data-emoji="cold_sweat" data-unicode-name="1F630"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F616" title="confounded" data-aliases="" data-emoji="confounded" data-unicode-name="1F616"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F615" title="confused" data-aliases="" data-emoji="confused" data-unicode-name="1F615"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F477" title="construction_worker" data-aliases="" data-emoji="construction_worker" data-unicode-name="1F477"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46E" title="cop" data-aliases="" data-emoji="cop" data-unicode-name="1F46E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46B" title="couple" data-aliases="" data-emoji="couple" data-unicode-name="1F46B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F491" title="couple_with_heart" data-aliases="" data-emoji="couple_with_heart" data-unicode-name="1F491"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F48F" title="couplekiss" data-aliases="" data-emoji="couplekiss" data-unicode-name="1F48F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F451" title="crown" data-aliases="" data-emoji="crown" data-unicode-name="1F451"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F622" title="cry" data-aliases="" data-emoji="cry" data-unicode-name="1F622"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63F" title="crying_cat_face" data-aliases="" data-emoji="crying_cat_face" data-unicode-name="1F63F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F498" title="cupid" data-aliases="" data-emoji="cupid" data-unicode-name="1F498"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F483" title="dancer" data-aliases="" data-emoji="dancer" data-unicode-name="1F483"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46F" title="dancers" data-aliases="" data-emoji="dancers" data-unicode-name="1F46F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A8" title="dash" data-aliases="" data-emoji="dash" data-unicode-name="1F4A8"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61E" title="disappointed" data-aliases="" data-emoji="disappointed" data-unicode-name="1F61E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F625" title="disappointed_relieved" data-aliases="" data-emoji="disappointed_relieved" data-unicode-name="1F625"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4AB" title="dizzy" data-aliases="" data-emoji="dizzy" data-unicode-name="1F4AB"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F635" title="dizzy_face" data-aliases="" data-emoji="dizzy_face" data-unicode-name="1F635"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F457" title="dress" data-aliases="" data-emoji="dress" data-unicode-name="1F457"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A7" title="droplet" data-aliases="" data-emoji="droplet" data-unicode-name="1F4A7"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F442" title="ear" data-aliases="" data-emoji="ear" data-unicode-name="1F442"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F611" title="expressionless" data-aliases="" data-emoji="expressionless" data-unicode-name="1F611"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F453" title="eyeglasses" data-aliases="" data-emoji="eyeglasses" data-unicode-name="1F453"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F440" title="eyes" data-aliases="" data-emoji="eyes" data-unicode-name="1F440"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46A" title="family" data-aliases="" data-emoji="family" data-unicode-name="1F46A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F628" title="fearful" data-aliases="" data-emoji="fearful" data-unicode-name="1F628"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F525" title="fire" data-aliases=":flame:" data-emoji="fire" data-unicode-name="1F525"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-270A" title="fist" data-aliases="" data-emoji="fist" data-unicode-name="270A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F633" title="flushed" data-aliases="" data-emoji="flushed" data-unicode-name="1F633"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F463" title="footprints" data-aliases="" data-emoji="footprints" data-unicode-name="1F463"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F626" title="frowning" data-aliases=":anguished:" data-emoji="frowning" data-unicode-name="1F626"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F48E" title="gem" data-aliases="" data-emoji="gem" data-unicode-name="1F48E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F467" title="girl" data-aliases="" data-emoji="girl" data-unicode-name="1F467"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F49A" title="green_heart" data-aliases="" data-emoji="green_heart" data-unicode-name="1F49A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62C" title="grimacing" data-aliases="" data-emoji="grimacing" data-unicode-name="1F62C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F601" title="grin" data-aliases="" data-emoji="grin" data-unicode-name="1F601"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F600" title="grinning" data-aliases="" data-emoji="grinning" data-unicode-name="1F600"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F482" title="guardsman" data-aliases="" data-emoji="guardsman" data-unicode-name="1F482"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F487" title="haircut" data-aliases="" data-emoji="haircut" data-unicode-name="1F487"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45C" title="handbag" data-aliases="" data-emoji="handbag" data-unicode-name="1F45C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F649" title="hear_no_evil" data-aliases="" data-emoji="hear_no_evil" data-unicode-name="1F649"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-2764" title="heart" data-aliases="" data-emoji="heart" data-unicode-name="2764"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60D" title="heart_eyes" data-aliases="" data-emoji="heart_eyes" data-unicode-name="1F60D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63B" title="heart_eyes_cat" data-aliases="" data-emoji="heart_eyes_cat" data-unicode-name="1F63B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F493" title="heartbeat" data-aliases="" data-emoji="heartbeat" data-unicode-name="1F493"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F497" title="heartpulse" data-aliases="" data-emoji="heartpulse" data-unicode-name="1F497"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F460" title="high_heel" data-aliases="" data-emoji="high_heel" data-unicode-name="1F460"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62F" title="hushed" data-aliases="" data-emoji="hushed" data-unicode-name="1F62F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F47F" title="imp" data-aliases="" data-emoji="imp" data-unicode-name="1F47F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F481" title="information_desk_person" data-aliases="" data-emoji="information_desk_person" data-unicode-name="1F481"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F607" title="innocent" data-aliases="" data-emoji="innocent" data-unicode-name="1F607"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F47A" title="japanese_goblin" data-aliases="" data-emoji="japanese_goblin" data-unicode-name="1F47A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F479" title="japanese_ogre" data-aliases="" data-emoji="japanese_ogre" data-unicode-name="1F479"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F456" title="jeans" data-aliases="" data-emoji="jeans" data-unicode-name="1F456"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F602" title="joy" data-aliases="" data-emoji="joy" data-unicode-name="1F602"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F639" title="joy_cat" data-aliases="" data-emoji="joy_cat" data-unicode-name="1F639"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F458" title="kimono" data-aliases="" data-emoji="kimono" data-unicode-name="1F458"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F48B" title="kiss" data-aliases="" data-emoji="kiss" data-unicode-name="1F48B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F617" title="kissing" data-aliases="" data-emoji="kissing" data-unicode-name="1F617"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63D" title="kissing_cat" data-aliases="" data-emoji="kissing_cat" data-unicode-name="1F63D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61A" title="kissing_closed_eyes" data-aliases="" data-emoji="kissing_closed_eyes" data-unicode-name="1F61A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F618" title="kissing_heart" data-aliases="" data-emoji="kissing_heart" data-unicode-name="1F618"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F619" title="kissing_smiling_eyes" data-aliases="" data-emoji="kissing_smiling_eyes" data-unicode-name="1F619"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F606" title="laughing" data-aliases=":satisfied:" data-emoji="laughing" data-unicode-name="1F606"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F444" title="lips" data-aliases="" data-emoji="lips" data-unicode-name="1F444"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F484" title="lipstick" data-aliases="" data-emoji="lipstick" data-unicode-name="1F484"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F48C" title="love_letter" data-aliases="" data-emoji="love_letter" data-unicode-name="1F48C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F468" title="man" data-aliases="" data-emoji="man" data-unicode-name="1F468"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F472" title="man_with_gua_pi_mao" data-aliases="" data-emoji="man_with_gua_pi_mao" data-unicode-name="1F472"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F473" title="man_with_turban" data-aliases="" data-emoji="man_with_turban" data-unicode-name="1F473"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45E" title="mans_shoe" data-aliases="" data-emoji="mans_shoe" data-unicode-name="1F45E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F637" title="mask" data-aliases="" data-emoji="mask" data-unicode-name="1F637"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F486" title="massage" data-aliases="" data-emoji="massage" data-unicode-name="1F486"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4AA" title="muscle" data-aliases="" data-emoji="muscle" data-unicode-name="1F4AA"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F485" title="nail_care" data-aliases="" data-emoji="nail_care" data-unicode-name="1F485"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F454" title="necktie" data-aliases="" data-emoji="necktie" data-unicode-name="1F454"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F610" title="neutral_face" data-aliases="" data-emoji="neutral_face" data-unicode-name="1F610"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F645" title="no_good" data-aliases="" data-emoji="no_good" data-unicode-name="1F645"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F636" title="no_mouth" data-aliases="" data-emoji="no_mouth" data-unicode-name="1F636"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F443" title="nose" data-aliases="" data-emoji="nose" data-unicode-name="1F443"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44C" title="ok_hand" data-aliases="" data-emoji="ok_hand" data-unicode-name="1F44C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F646" title="ok_woman" data-aliases="" data-emoji="ok_woman" data-unicode-name="1F646"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F474" title="older_man" data-aliases="" data-emoji="older_man" data-unicode-name="1F474"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F475" title="older_woman" data-aliases=":grandma:" data-emoji="older_woman" data-unicode-name="1F475"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F450" title="open_hands" data-aliases="" data-emoji="open_hands" data-unicode-name="1F450"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62E" title="open_mouth" data-aliases="" data-emoji="open_mouth" data-unicode-name="1F62E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F614" title="pensive" data-aliases="" data-emoji="pensive" data-unicode-name="1F614"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F623" title="persevere" data-aliases="" data-emoji="persevere" data-unicode-name="1F623"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64D" title="person_frowning" data-aliases="" data-emoji="person_frowning" data-unicode-name="1F64D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F471" title="person_with_blond_hair" data-aliases="" data-emoji="person_with_blond_hair" data-unicode-name="1F471"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64E" title="person_with_pouting_face" data-aliases="" data-emoji="person_with_pouting_face" data-unicode-name="1F64E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F447" title="point_down" data-aliases="" data-emoji="point_down" data-unicode-name="1F447"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F448" title="point_left" data-aliases="" data-emoji="point_left" data-unicode-name="1F448"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F449" title="point_right" data-aliases="" data-emoji="point_right" data-unicode-name="1F449"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-261D" title="point_up" data-aliases="" data-emoji="point_up" data-unicode-name="261D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F446" title="point_up_2" data-aliases="" data-emoji="point_up_2" data-unicode-name="1F446"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A9" title="poop" data-aliases=":shit: :hankey: :poo:" data-emoji="poop" data-unicode-name="1F4A9"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45D" title="pouch" data-aliases="" data-emoji="pouch" data-unicode-name="1F45D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63E" title="pouting_cat" data-aliases="" data-emoji="pouting_cat" data-unicode-name="1F63E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64F" title="pray" data-aliases="" data-emoji="pray" data-unicode-name="1F64F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F478" title="princess" data-aliases="" data-emoji="princess" data-unicode-name="1F478"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44A" title="punch" data-aliases="" data-emoji="punch" data-unicode-name="1F44A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F49C" title="purple_heart" data-aliases="" data-emoji="purple_heart" data-unicode-name="1F49C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45B" title="purse" data-aliases="" data-emoji="purse" data-unicode-name="1F45B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F621" title="rage" data-aliases="" data-emoji="rage" data-unicode-name="1F621"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-270B" title="raised_hand" data-aliases="" data-emoji="raised_hand" data-unicode-name="270B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64C" title="raised_hands" data-aliases="" data-emoji="raised_hands" data-unicode-name="1F64C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64B" title="raising_hand" data-aliases="" data-emoji="raising_hand" data-unicode-name="1F64B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-263A" title="relaxed" data-aliases="" data-emoji="relaxed" data-unicode-name="263A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60C" title="relieved" data-aliases="" data-emoji="relieved" data-unicode-name="1F60C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F49E" title="revolving_hearts" data-aliases="" data-emoji="revolving_hearts" data-unicode-name="1F49E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F380" title="ribbon" data-aliases="" data-emoji="ribbon" data-unicode-name="1F380"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F48D" title="ring" data-aliases="" data-emoji="ring" data-unicode-name="1F48D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F3C3" title="runner" data-aliases="" data-emoji="runner" data-unicode-name="1F3C3"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F3BD" title="running_shirt_with_sash" data-aliases="" data-emoji="running_shirt_with_sash" data-unicode-name="1F3BD"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F461" title="sandal" data-aliases="" data-emoji="sandal" data-unicode-name="1F461"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F631" title="scream" data-aliases="" data-emoji="scream" data-unicode-name="1F631"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F640" title="scream_cat" data-aliases="" data-emoji="scream_cat" data-unicode-name="1F640"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F648" title="see_no_evil" data-aliases="" data-emoji="see_no_evil" data-unicode-name="1F648"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F455" title="shirt" data-aliases="" data-emoji="shirt" data-unicode-name="1F455"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F480" title="skull" data-aliases=":skeleton:" data-emoji="skull" data-unicode-name="1F480"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F634" title="sleeping" data-aliases="" data-emoji="sleeping" data-unicode-name="1F634"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62A" title="sleepy" data-aliases="" data-emoji="sleepy" data-unicode-name="1F62A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F604" title="smile" data-aliases="" data-emoji="smile" data-unicode-name="1F604"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F638" title="smile_cat" data-aliases="" data-emoji="smile_cat" data-unicode-name="1F638"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F603" title="smiley" data-aliases="" data-emoji="smiley" data-unicode-name="1F603"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63A" title="smiley_cat" data-aliases="" data-emoji="smiley_cat" data-unicode-name="1F63A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F608" title="smiling_imp" data-aliases="" data-emoji="smiling_imp" data-unicode-name="1F608"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60F" title="smirk" data-aliases="" data-emoji="smirk" data-unicode-name="1F60F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F63C" title="smirk_cat" data-aliases="" data-emoji="smirk_cat" data-unicode-name="1F63C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62D" title="sob" data-aliases="" data-emoji="sob" data-unicode-name="1F62D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-2728" title="sparkles" data-aliases="" data-emoji="sparkles" data-unicode-name="2728"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F496" title="sparkling_heart" data-aliases="" data-emoji="sparkling_heart" data-unicode-name="1F496"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F64A" title="speak_no_evil" data-aliases="" data-emoji="speak_no_evil" data-unicode-name="1F64A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4AC" title="speech_balloon" data-aliases="" data-emoji="speech_balloon" data-unicode-name="1F4AC"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F31F" title="star2" data-aliases="" data-emoji="star2" data-unicode-name="1F31F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61B" title="stuck_out_tongue" data-aliases="" data-emoji="stuck_out_tongue" data-unicode-name="1F61B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61D" title="stuck_out_tongue_closed_eyes" data-aliases="" data-emoji="stuck_out_tongue_closed_eyes" data-unicode-name="1F61D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61C" title="stuck_out_tongue_winking_eye" data-aliases="" data-emoji="stuck_out_tongue_winking_eye" data-unicode-name="1F61C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60E" title="sunglasses" data-aliases="" data-emoji="sunglasses" data-unicode-name="1F60E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F613" title="sweat" data-aliases="" data-emoji="sweat" data-unicode-name="1F613"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A6" title="sweat_drops" data-aliases="" data-emoji="sweat_drops" data-unicode-name="1F4A6"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F605" title="sweat_smile" data-aliases="" data-emoji="sweat_smile" data-unicode-name="1F605"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4AD" title="thought_balloon" data-aliases="" data-emoji="thought_balloon" data-unicode-name="1F4AD"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44E" title="thumbsdown" data-aliases=":-1:" data-emoji="thumbsdown" data-unicode-name="1F44E"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44D" title="thumbsup" data-aliases=":+1:" data-emoji="thumbsup" data-unicode-name="1F44D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F62B" title="tired_face" data-aliases="" data-emoji="tired_face" data-unicode-name="1F62B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F445" title="tongue" data-aliases="" data-emoji="tongue" data-unicode-name="1F445"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F3A9" title="tophat" data-aliases="" data-emoji="tophat" data-unicode-name="1F3A9"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F624" title="triumph" data-aliases="" data-emoji="triumph" data-unicode-name="1F624"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F495" title="two_hearts" data-aliases="" data-emoji="two_hearts" data-unicode-name="1F495"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46C" title="two_men_holding_hands" data-aliases="" data-emoji="two_men_holding_hands" data-unicode-name="1F46C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F46D" title="two_women_holding_hands" data-aliases="" data-emoji="two_women_holding_hands" data-unicode-name="1F46D"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F612" title="unamused" data-aliases="" data-emoji="unamused" data-unicode-name="1F612"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-270C" title="v" data-aliases="" data-emoji="v" data-unicode-name="270C"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F6B6" title="walking" data-aliases="" data-emoji="walking" data-unicode-name="1F6B6"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F44B" title="wave" data-aliases="" data-emoji="wave" data-unicode-name="1F44B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F629" title="weary" data-aliases="" data-emoji="weary" data-unicode-name="1F629"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F609" title="wink" data-aliases="" data-emoji="wink" data-unicode-name="1F609"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F469" title="woman" data-aliases="" data-emoji="woman" data-unicode-name="1F469"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F45A" title="womans_clothes" data-aliases="" data-emoji="womans_clothes" data-unicode-name="1F45A"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F452" title="womans_hat" data-aliases="" data-emoji="womans_hat" data-unicode-name="1F452"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F61F" title="worried" data-aliases="" data-emoji="worried" data-unicode-name="1F61F"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F49B" title="yellow_heart" data-aliases="" data-emoji="yellow_heart" data-unicode-name="1F49B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F60B" title="yum" data-aliases="" data-emoji="yum" data-unicode-name="1F60B"></div>
+ </button>
+ </li>
+ <li class='pull-left text-center emoji-menu-list-item'>
+ <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
+ <div class="icon emoji-icon emoji-1F4A4" title="zzz" data-aliases="" data-emoji="zzz" data-unicode-name="1F4A4"></div>
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+"""
diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml
new file mode 100644
index 00000000000..95efaff4b69
--- /dev/null
+++ b/spec/javascripts/fixtures/right_sidebar.html.haml
@@ -0,0 +1,13 @@
+%div
+ %div.page-gutter.page-with-sidebar
+
+ %aside.right-sidebar
+ %div.block.issuable-sidebar-header
+ %a.gutter-toggle.pull-right.js-sidebar-toggle
+ %i.fa.fa-angle-double-left
+
+ %form.issuable-context-form
+ %div.block.labels
+ %div.sidebar-collapsed-icon
+ %i.fa.fa-tags
+ %span 1
diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml
new file mode 100644
index 00000000000..859e79a6c9e
--- /dev/null
+++ b/spec/javascripts/fixtures/u2f/authenticate.html.haml
@@ -0,0 +1 @@
+= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in" }
diff --git a/spec/javascripts/fixtures/u2f/register.html.haml b/spec/javascripts/fixtures/u2f/register.html.haml
new file mode 100644
index 00000000000..393c0613fd3
--- /dev/null
+++ b/spec/javascripts/fixtures/u2f/register.html.haml
@@ -0,0 +1 @@
+= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f' }
diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index 78d39f1b428..82ee1954a59 100644
--- a/spec/javascripts/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,4 +1,4 @@
-//= require stat_graph_contributors_graph
+//= require graphs/stat_graph_contributors_graph
describe("ContributorsGraph", function () {
describe("#set_x_domain", function () {
diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index dbafe782b77..56970e22e34 100644
--- a/spec/javascripts/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,4 +1,4 @@
-//= require stat_graph_contributors_util
+//= require graphs/stat_graph_contributors_util
describe("ContributorsStatGraphUtil", function () {
@@ -9,14 +9,14 @@ describe("ContributorsStatGraphUtil", function () {
{author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1},
{author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3},
{author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}]
-
+
var correct_parsed_log = {
total: [
{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
{date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
by_author:
[
- {
+ {
author_name: "Karlo Soriano", author_email: "karlo@email.com",
"2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
},
@@ -132,8 +132,8 @@ describe("ContributorsStatGraphUtil", function () {
total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
{date: "2013-05-08", additions: 54, deletions: 7, commits: 3}],
by_author:[
- {
- author: "Karlo Soriano",
+ {
+ author: "Karlo Soriano",
"2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
},
{
@@ -161,11 +161,11 @@ describe("ContributorsStatGraphUtil", function () {
it("returns the log by author sorted by specified field", function () {
var fake_parsed_log = {
total: [
- {date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
+ {date: "2013-05-09", additions: 471, deletions: 0, commits: 1},
{date: "2013-05-08", additions: 54, deletions: 7, commits: 3}
],
by_author: [
- {
+ {
author_name: "Karlo Soriano", author_email: "karlo@email.com",
"2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}
},
diff --git a/spec/javascripts/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index 4c652910cd6..4b05d401a42 100644
--- a/spec/javascripts/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,4 +1,4 @@
-//= require stat_graph
+//= require graphs/stat_graph
describe("StatGraph", function () {
diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee
index f2ce85efcdc..ce773793817 100644
--- a/spec/javascripts/new_branch_spec.js.coffee
+++ b/spec/javascripts/new_branch_spec.js.coffee
@@ -1,4 +1,4 @@
-#= require jquery-ui
+#= require jquery-ui/autocomplete
#= require new_branch_form
describe 'Branch', ->
diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee
index 3d8de2ff989..1cf34d4d2d3 100644
--- a/spec/javascripts/project_title_spec.js.coffee
+++ b/spec/javascripts/project_title_spec.js.coffee
@@ -1,5 +1,6 @@
#= require bootstrap
#= require select2
+#= require lib/type_utility
#= require gl_dropdown
#= require api
#= require project_select
diff --git a/spec/javascripts/right_sidebar_spec.js.coffee b/spec/javascripts/right_sidebar_spec.js.coffee
new file mode 100644
index 00000000000..2075cacdb67
--- /dev/null
+++ b/spec/javascripts/right_sidebar_spec.js.coffee
@@ -0,0 +1,69 @@
+#= require right_sidebar
+#= require jquery
+#= require jquery.cookie
+
+@sidebar = null
+$aside = null
+$toggle = null
+$icon = null
+$page = null
+$labelsIcon = null
+
+
+assertSidebarState = (state) ->
+
+ shouldBeExpanded = state is 'expanded'
+ shouldBeCollapsed = state is 'collapsed'
+
+ expect($aside.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded
+ expect($page.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded
+ expect($icon.hasClass('fa-angle-double-right')).toBe shouldBeExpanded
+
+ expect($aside.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed
+ expect($page.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed
+ expect($icon.hasClass('fa-angle-double-left')).toBe shouldBeCollapsed
+
+
+describe 'RightSidebar', ->
+
+ fixture.preload 'right_sidebar.html'
+
+ beforeEach ->
+ fixture.load 'right_sidebar.html'
+
+ @sidebar = new Sidebar
+ $aside = $ '.right-sidebar'
+ $page = $ '.page-with-sidebar'
+ $icon = $aside.find 'i'
+ $toggle = $aside.find '.js-sidebar-toggle'
+ $labelsIcon = $aside.find '.sidebar-collapsed-icon'
+
+
+ it 'should expand the sidebar when arrow is clicked', ->
+
+ $toggle.click()
+ assertSidebarState 'expanded'
+
+
+ it 'should collapse the sidebar when arrow is clicked', ->
+
+ $toggle.click()
+ assertSidebarState 'expanded'
+
+ $toggle.click()
+ assertSidebarState 'collapsed'
+
+
+ it 'should float over the page and when sidebar icons clicked', ->
+
+ $labelsIcon.click()
+ assertSidebarState 'expanded'
+
+
+ it 'should collapse when the icon arrow clicked while it is floating on page', ->
+
+ $labelsIcon.click()
+ assertSidebarState 'expanded'
+
+ $toggle.click()
+ assertSidebarState 'collapsed'
diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee
new file mode 100644
index 00000000000..e8a2892d678
--- /dev/null
+++ b/spec/javascripts/u2f/authenticate_spec.coffee
@@ -0,0 +1,52 @@
+#= require u2f/authenticate
+#= require u2f/util
+#= require u2f/error
+#= require u2f
+#= require ./mock_u2f_device
+
+describe 'U2FAuthenticate', ->
+ U2FUtil.enableTestMode()
+ fixture.load('u2f/authenticate')
+
+ beforeEach ->
+ @u2fDevice = new MockU2FDevice
+ @container = $("#js-authenticate-u2f")
+ @component = new U2FAuthenticate(@container, {}, "token")
+ @component.start()
+
+ it 'allows authenticating via a U2F device', ->
+ setupButton = @container.find("#js-login-u2f-device")
+ setupMessage = @container.find("p")
+ expect(setupMessage.text()).toContain('Insert your security key')
+ expect(setupButton.text()).toBe('Login Via U2F Device')
+ setupButton.trigger('click')
+
+ inProgressMessage = @container.find("p")
+ expect(inProgressMessage.text()).toContain("Trying to communicate with your device")
+
+ @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"})
+ authenticatedMessage = @container.find("p")
+ deviceResponse = @container.find('#js-device-response')
+ expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server")
+ expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}')
+
+ describe "errors", ->
+ it "displays an error message", ->
+ setupButton = @container.find("#js-login-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"})
+ errorMessage = @container.find("p")
+ expect(errorMessage.text()).toContain("There was a problem communicating with your device")
+
+ it "allows retrying authentication after an error", ->
+ setupButton = @container.find("#js-login-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"})
+ retryButton = @container.find("#js-u2f-try-again")
+ retryButton.trigger('click')
+
+ setupButton = @container.find("#js-login-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"})
+ authenticatedMessage = @container.find("p")
+ expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server")
diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee
new file mode 100644
index 00000000000..97ed0e83a0e
--- /dev/null
+++ b/spec/javascripts/u2f/mock_u2f_device.js.coffee
@@ -0,0 +1,15 @@
+class @MockU2FDevice
+ constructor: () ->
+ window.u2f ||= {}
+
+ window.u2f.register = (appId, registerRequests, signRequests, callback) =>
+ @registerCallback = callback
+
+ window.u2f.sign = (appId, challenges, signRequests, callback) =>
+ @authenticateCallback = callback
+
+ respondToRegisterRequest: (params) =>
+ @registerCallback(params)
+
+ respondToAuthenticateRequest: (params) =>
+ @authenticateCallback(params)
diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee
new file mode 100644
index 00000000000..0858abeca1a
--- /dev/null
+++ b/spec/javascripts/u2f/register_spec.js.coffee
@@ -0,0 +1,57 @@
+#= require u2f/register
+#= require u2f/util
+#= require u2f/error
+#= require u2f
+#= require ./mock_u2f_device
+
+describe 'U2FRegister', ->
+ U2FUtil.enableTestMode()
+ fixture.load('u2f/register')
+
+ beforeEach ->
+ @u2fDevice = new MockU2FDevice
+ @container = $("#js-register-u2f")
+ @component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token")
+ @component.start()
+
+ it 'allows registering a U2F device', ->
+ setupButton = @container.find("#js-setup-u2f-device")
+ expect(setupButton.text()).toBe('Setup New U2F Device')
+ setupButton.trigger('click')
+
+ inProgressMessage = @container.children("p")
+ expect(inProgressMessage.text()).toContain("Trying to communicate with your device")
+
+ @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"})
+ registeredMessage = @container.find('p')
+ deviceResponse = @container.find('#js-device-response')
+ expect(registeredMessage.text()).toContain("Your device was successfully set up!")
+ expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}')
+
+ describe "errors", ->
+ it "doesn't allow the same device to be registered twice (for the same user", ->
+ setupButton = @container.find("#js-setup-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToRegisterRequest({errorCode: 4})
+ errorMessage = @container.find("p")
+ expect(errorMessage.text()).toContain("already been registered with us")
+
+ it "displays an error message for other errors", ->
+ setupButton = @container.find("#js-setup-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToRegisterRequest({errorCode: "error!"})
+ errorMessage = @container.find("p")
+ expect(errorMessage.text()).toContain("There was a problem communicating with your device")
+
+ it "allows retrying registration after an error", ->
+ setupButton = @container.find("#js-setup-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToRegisterRequest({errorCode: "error!"})
+ retryButton = @container.find("#U2FTryAgain")
+ retryButton.trigger('click')
+
+ setupButton = @container.find("#js-setup-u2f-device")
+ setupButton.trigger('click')
+ @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"})
+ registeredMessage = @container.find("p")
+ expect(registeredMessage.text()).toContain("Your device was successfully set up!")
diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
index c2a8ad36c30..593bd6d5cac 100644
--- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
@@ -98,11 +98,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit_range]).not_to be_empty
- end
end
context 'cross-project reference' do
@@ -135,11 +130,6 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit_range]).not_to be_empty
- end
end
context 'cross-project URL reference' do
@@ -173,10 +163,5 @@ describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit_range]).not_to be_empty
- end
end
end
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index 63a32d9d455..d46d3f1489e 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -93,11 +93,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit]).not_to be_empty
- end
end
context 'cross-project reference' do
@@ -124,11 +119,6 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
exp = act = "Committed #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit]).not_to be_empty
- end
end
context 'cross-project URL reference' do
@@ -154,10 +144,5 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do
act = "Committed #{invalidate_reference(reference)}"
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("See #{reference}")
- expect(result[:references][:commit]).not_to be_empty
- end
end
end
diff --git a/spec/lib/banzai/filter/inline_diff_filter_spec.rb b/spec/lib/banzai/filter/inline_diff_filter_spec.rb
new file mode 100644
index 00000000000..9e526371294
--- /dev/null
+++ b/spec/lib/banzai/filter/inline_diff_filter_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe Banzai::Filter::InlineDiffFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'adds inline diff span tags for deletions when using square brackets' do
+ doc = "START [-something deleted-] END"
+ expect(filter(doc).to_html).to eq('START <span class="idiff left right deletion">something deleted</span> END')
+ end
+
+ it 'adds inline diff span tags for deletions when using curley braces' do
+ doc = "START {-something deleted-} END"
+ expect(filter(doc).to_html).to eq('START <span class="idiff left right deletion">something deleted</span> END')
+ end
+
+ it 'does not add inline diff span tags when a closing tag is not provided' do
+ doc = "START [- END"
+ expect(filter(doc).to_html).to eq(doc)
+ end
+
+ it 'adds inline span tags for additions when using square brackets' do
+ doc = "START [+something added+] END"
+ expect(filter(doc).to_html).to eq('START <span class="idiff left right addition">something added</span> END')
+ end
+
+ it 'adds inline span tags for additions when using curley braces' do
+ doc = "START {+something added+} END"
+ expect(filter(doc).to_html).to eq('START <span class="idiff left right addition">something added</span> END')
+ end
+
+ it 'does not add inline diff span tags when a closing addition tag is not provided' do
+ doc = "START {+ END"
+ expect(filter(doc).to_html).to eq(doc)
+ end
+
+ it 'does not add inline diff span tags when the tags do not match' do
+ examples = [
+ "{+ additions +]",
+ "[+ additions +}",
+ "{- delletions -]",
+ "[- delletions -}"
+ ]
+
+ examples.each do |doc|
+ expect(filter(doc).to_html).to eq(doc)
+ end
+ end
+
+ it 'prevents user-land html being injected' do
+ doc = "START {+&lt;script&gt;alert('I steal cookies')&lt;/script&gt;+} END"
+ expect(filter(doc).to_html).to eq("START <span class=\"idiff left right addition\">&lt;script&gt;alert('I steal cookies')&lt;/script&gt;</span> END")
+ end
+
+ it 'preserves content inside pre tags' do
+ doc = "<pre>START {+something added+} END</pre>"
+ expect(filter(doc).to_html).to eq(doc)
+ end
+
+ it 'preserves content inside code tags' do
+ doc = "<code>START {+something added+} END</code>"
+ expect(filter(doc).to_html).to eq(doc)
+ end
+
+ it 'preserves content inside tt tags' do
+ doc = "<tt>START {+something added+} END</tt>"
+ expect(filter(doc).to_html).to eq(doc)
+ end
+end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 266ebef33d6..8e6a264970d 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -91,11 +91,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true)
end
- it 'adds to the results hash' do
- result = reference_pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
- end
-
it 'does not process links containing issue numbers followed by text' do
href = "#{reference}st"
doc = reference_filter("<a href='#{href}'></a>")
@@ -136,11 +131,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
- end
end
context 'cross-project URL reference' do
@@ -160,11 +150,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
- end
end
context 'cross-project reference in link href' do
@@ -184,11 +169,6 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
- end
end
context 'cross-project URL in link href' do
@@ -208,10 +188,5 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
doc = reference_filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Fixed #{reference}")
- expect(result[:references][:issue]).to eq [issue]
- end
end
end
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index b0a38e7c251..f1064a701d8 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -48,11 +48,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(link).to eq urls.namespace_project_issues_path(project.namespace, project, label_name: label.name)
end
- it 'adds to the results hash' do
- result = reference_pipeline_result("Label #{reference}")
- expect(result[:references][:label]).to eq [label]
- end
-
describe 'label span element' do
it 'includes default classes' do
doc = reference_filter("Label #{reference}")
@@ -170,11 +165,6 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(link).to have_attribute('data-label')
expect(link.attr('data-label')).to eq label.id.to_s
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Label #{reference}")
- expect(result[:references][:label]).to eq [label]
- end
end
describe 'cross project label references' do
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index 352710df307..3185e41fe5c 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -78,11 +78,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Merge #{reference}")
- expect(result[:references][:merge_request]).to eq [merge]
- end
end
context 'cross-project reference' do
@@ -109,11 +104,6 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Merge #{reference}")
- expect(result[:references][:merge_request]).to eq [merge]
- end
end
context 'cross-project URL reference' do
@@ -133,10 +123,5 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
doc = reference_filter("Merge (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Merge #{reference}")
- expect(result[:references][:merge_request]).to eq [merge]
- end
end
end
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index 5beb61dac5c..9424f2363e1 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -3,8 +3,9 @@ require 'spec_helper'
describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
include FilterSpecHelper
- let(:project) { create(:project, :public) }
- let(:milestone) { create(:milestone, project: project) }
+ let(:project) { create(:project, :public) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:reference) { milestone.to_reference }
it 'requires project context' do
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
@@ -17,11 +18,37 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
end
end
- context 'internal reference' do
- # Convert the Markdown link to only the URL, since these tests aren't run through the regular Markdown pipeline.
- # Milestone reference behavior in the full Markdown pipeline is tested elsewhere.
- let(:reference) { milestone.to_reference.gsub(/\[([^\]]+)\]\(([^)]+)\)/, '\2') }
+ it 'includes default classes' do
+ doc = reference_filter("Milestone #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Milestone #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-milestone attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-milestone')
+ expect(link.attr('data-milestone')).to eq milestone.id.to_s
+ end
+
+ it 'supports an :only_path context' do
+ doc = reference_filter("Milestone #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.
+ namespace_project_milestone_path(project.namespace, project, milestone)
+ end
+
+ context 'Integer-based references' do
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
@@ -30,29 +57,82 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
end
it 'links with adjacent text' do
- doc = reference_filter("milestone (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(milestone.title)}<\/a>\.\)/)
+ doc = reference_filter("Milestone (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+>#{milestone.name}</a>\.\)))
end
- it 'includes a title attribute' do
- doc = reference_filter("milestone #{reference}")
- expect(doc.css('a').first.attr('title')).to eq "Milestone: #{milestone.title}"
+ it 'ignores invalid milestone IIDs' do
+ exp = act = "Milestone #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
end
+ end
+
+ context 'String-based single-word references' do
+ let(:milestone) { create(:milestone, name: 'gfm', project: project) }
+ let(:reference) { "#{Milestone.reference_prefix}#{milestone.name}" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
- it 'escapes the title attribute' do
- milestone.update_attribute(:title, %{"></a>whatever<a title="})
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_milestone_url(project.namespace, project, milestone)
+ expect(doc.text).to eq 'See gfm'
+ end
- doc = reference_filter("milestone #{reference}")
- expect(doc.text).to eq "milestone \">whatever"
+ it 'links with adjacent text' do
+ doc = reference_filter("Milestone (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+>#{milestone.name}</a>\.\)))
end
- it 'includes default classes' do
- doc = reference_filter("milestone #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone'
+ it 'ignores invalid milestone names' do
+ exp = act = "Milestone #{Milestone.reference_prefix}#{milestone.name.reverse}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'String-based multi-word references in quotes' do
+ let(:milestone) { create(:milestone, name: 'gfm references', project: project) }
+ let(:reference) { milestone.to_reference(format: :name) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_milestone_url(project.namespace, project, milestone)
+ expect(doc.text).to eq 'See gfm references'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Milestone (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+>#{milestone.name}</a>\.\)))
+ end
+
+ it 'ignores invalid milestone names' do
+ exp = act = %(Milestone #{Milestone.reference_prefix}"#{milestone.name.reverse}")
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ describe 'referencing a milestone in a link href' do
+ let(:reference) { %Q{<a href="#{milestone.to_reference}">Milestone</a>} }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_milestone_url(project.namespace, project, milestone)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Milestone (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+>Milestone</a>\.\)))
end
it 'includes a data-project attribute' do
- doc = reference_filter("milestone #{reference}")
+ doc = reference_filter("Milestone #{reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-project')
@@ -66,10 +146,31 @@ describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
expect(link).to have_attribute('data-milestone')
expect(link.attr('data-milestone')).to eq milestone.id.to_s
end
+ end
+
+ describe 'cross project milestone references' do
+ let(:another_project) { create(:empty_project, :public) }
+ let(:project_path) { another_project.path_with_namespace }
+ let(:milestone) { create(:milestone, project: another_project) }
+ let(:reference) { milestone.to_reference(project) }
+
+ let!(:result) { reference_filter("See #{reference}") }
+
+ it 'points to referenced project milestone page' do
+ expect(result.css('a').first.attr('href')).to eq urls.
+ namespace_project_milestone_url(another_project.namespace,
+ another_project,
+ milestone)
+ end
- it 'adds to the results hash' do
- result = reference_pipeline_result("milestone #{reference}")
- expect(result[:references][:milestone]).to eq [milestone]
+ it 'contains cross project content' do
+ expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}"
+ end
+
+ it 'escapes the name attribute' do
+ allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="})
+ doc = reference_filter("See #{reference}")
+ expect(doc.css('a').first.text).to eq "#{milestone.name} in #{project_path}"
end
end
end
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index c2c2fd0eb6a..f181125156b 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -16,11 +16,23 @@ describe Banzai::Filter::RedactorFilter, lib: true do
end
context 'with data-project' do
+ let(:parser_class) do
+ Class.new(Banzai::ReferenceParser::BaseParser) do
+ self.reference_type = :test
+ end
+ end
+
+ before do
+ allow(Banzai::ReferenceParser).to receive(:[]).
+ with('test').
+ and_return(parser_class)
+ end
+
it 'removes unpermitted Project references' do
user = create(:user)
project = create(:empty_project)
- link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
+ link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0
@@ -31,14 +43,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project)
project.team << [user, :master]
- link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
+ link = reference_link(project: project.id, reference_type: 'test')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
end
it 'handles invalid Project references' do
- link = reference_link(project: 12345, reference_filter: 'ReferenceFilter')
+ link = reference_link(project: 12345, reference_type: 'test')
expect { filter(link) }.not_to raise_error
end
@@ -51,18 +63,30 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project)
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: non_member)
expect(doc.css('a').length).to eq 0
end
+ it 'removes references for project members with guest role' do
+ member = create(:user)
+ project = create(:empty_project, :public)
+ project.team << [member, :guest]
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
+ doc = filter(link, current_user: member)
+
+ expect(doc.css('a').length).to eq 0
+ end
+
it 'allows references for author' do
author = create(:user)
project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project, author: author)
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: author)
expect(doc.css('a').length).to eq 1
@@ -73,7 +97,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project, assignee: assignee)
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: assignee)
expect(doc.css('a').length).to eq 1
@@ -85,7 +109,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project.team << [member, :developer]
issue = create(:issue, :confidential, project: project)
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: member)
expect(doc.css('a').length).to eq 1
@@ -96,7 +120,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, :confidential, project: project)
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: admin)
expect(doc.css('a').length).to eq 1
@@ -108,7 +132,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
project = create(:empty_project, :public)
issue = create(:issue, project: project)
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ link = reference_link(project: project.id, issue: issue.id, reference_type: 'issue')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
@@ -121,7 +145,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
user = create(:user)
group = create(:group, :private)
- link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
+ link = reference_link(group: group.id, reference_type: 'user')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0
@@ -132,14 +156,14 @@ describe Banzai::Filter::RedactorFilter, lib: true do
group = create(:group, :private)
group.add_developer(user)
- link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
+ link = reference_link(group: group.id, reference_type: 'user')
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
end
it 'handles invalid Group references' do
- link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter')
+ link = reference_link(group: 12345, reference_type: 'user')
expect { filter(link) }.not_to raise_error
end
@@ -149,7 +173,7 @@ describe Banzai::Filter::RedactorFilter, lib: true do
it 'allows any User reference' do
user = create(:user)
- link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter')
+ link = reference_link(user: user.id, reference_type: 'user')
doc = filter(link)
expect(doc.css('a').length).to eq 1
diff --git a/spec/lib/banzai/filter/reference_filter_spec.rb b/spec/lib/banzai/filter/reference_filter_spec.rb
new file mode 100644
index 00000000000..55e681f6faf
--- /dev/null
+++ b/spec/lib/banzai/filter/reference_filter_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Banzai::Filter::ReferenceFilter, lib: true do
+ let(:project) { build(:project) }
+
+ describe '#each_node' do
+ it 'iterates over the nodes in a document' do
+ document = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
+ filter = described_class.new(document, project: project)
+
+ expect { |b| filter.each_node(&b) }.
+ to yield_with_args(an_instance_of(Nokogiri::XML::Element))
+ end
+
+ it 'returns an Enumerator when no block is given' do
+ document = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
+ filter = described_class.new(document, project: project)
+
+ expect(filter.each_node).to be_an_instance_of(Enumerator)
+ end
+
+ it 'skips links with a "gfm" class' do
+ document = Nokogiri::HTML.fragment('<a href="foo" class="gfm">foo</a>')
+ filter = described_class.new(document, project: project)
+
+ expect { |b| filter.each_node(&b) }.not_to yield_control
+ end
+
+ it 'skips text nodes in pre elements' do
+ document = Nokogiri::HTML.fragment('<pre>foo</pre>')
+ filter = described_class.new(document, project: project)
+
+ expect { |b| filter.each_node(&b) }.not_to yield_control
+ end
+ end
+
+ describe '#nodes' do
+ it 'returns an Array of the HTML nodes' do
+ document = Nokogiri::HTML.fragment('<a href="foo">foo</a>')
+ filter = described_class.new(document, project: project)
+
+ expect(filter.nodes).to eq([document.children[0]])
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/reference_gatherer_filter_spec.rb b/spec/lib/banzai/filter/reference_gatherer_filter_spec.rb
deleted file mode 100644
index c8b1dfdf944..00000000000
--- a/spec/lib/banzai/filter/reference_gatherer_filter_spec.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::Filter::ReferenceGathererFilter, lib: true do
- include ActionView::Helpers::UrlHelper
- include FilterSpecHelper
-
- def reference_link(data)
- link_to('text', '', class: 'gfm', data: data)
- end
-
- context "for issue references" do
-
- context 'with data-project' do
- it 'removes unpermitted Project references' do
- user = create(:user)
- project = create(:empty_project)
- issue = create(:issue, project: project)
-
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
- result = pipeline_result(link, current_user: user)
-
- expect(result[:references][:issue]).to be_empty
- end
-
- it 'allows permitted Project references' do
- user = create(:user)
- project = create(:empty_project)
- issue = create(:issue, project: project)
- project.team << [user, :master]
-
- link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
- result = pipeline_result(link, current_user: user)
-
- expect(result[:references][:issue]).to eq([issue])
- end
-
- it 'handles invalid Project references' do
- link = reference_link(project: 12345, issue: 12345, reference_filter: 'IssueReferenceFilter')
-
- expect { pipeline_result(link) }.not_to raise_error
- end
- end
- end
-
- context "for user references" do
-
- context 'with data-group' do
- it 'removes unpermitted Group references' do
- user = create(:user)
- group = create(:group)
-
- link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
- result = pipeline_result(link, current_user: user)
-
- expect(result[:references][:user]).to be_empty
- end
-
- it 'allows permitted Group references' do
- user = create(:user)
- group = create(:group)
- group.add_developer(user)
-
- link = reference_link(group: group.id, reference_filter: 'UserReferenceFilter')
- result = pipeline_result(link, current_user: user)
-
- expect(result[:references][:user]).to eq([user])
- end
-
- it 'handles invalid Group references' do
- link = reference_link(group: 12345, reference_filter: 'UserReferenceFilter')
-
- expect { pipeline_result(link) }.not_to raise_error
- end
- end
-
- context 'with data-user' do
- it 'allows any User reference' do
- user = create(:user)
-
- link = reference_link(user: user.id, reference_filter: 'UserReferenceFilter')
- result = pipeline_result(link)
-
- expect(result[:references][:user]).to eq([user])
- end
- end
- end
-end
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index 26466fbb180..5068ddd7faa 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -77,11 +77,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Snippet #{reference}")
- expect(result[:references][:snippet]).to eq [snippet]
- end
end
context 'cross-project reference' do
@@ -107,11 +102,6 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Snippet #{reference}")
- expect(result[:references][:snippet]).to eq [snippet]
- end
end
context 'cross-project URL reference' do
@@ -137,10 +127,5 @@ describe Banzai::Filter::SnippetReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Snippet #{reference}")
- expect(result[:references][:snippet]).to eq [snippet]
- end
end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 8bdebae1841..108b36a97cc 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -31,28 +31,22 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
it 'supports a special @all mention' do
- doc = reference_filter("Hey #{reference}")
+ doc = reference_filter("Hey #{reference}", author: user)
expect(doc.css('a').length).to eq 1
expect(doc.css('a').first.attr('href'))
.to eq urls.namespace_project_url(project.namespace, project)
end
- context "when the author is a member of the project" do
+ it 'includes a data-author attribute when there is an author' do
+ doc = reference_filter(reference, author: user)
- it 'adds to the results hash' do
- result = reference_pipeline_result("Hey #{reference}", author: project.creator)
- expect(result[:references][:user]).to eq [project.creator]
- end
+ expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s)
end
- context "when the author is not a member of the project" do
-
- let(:other_user) { create(:user) }
+ it 'does not include a data-author attribute when there is no author' do
+ doc = reference_filter(reference)
- it "doesn't add to the results hash" do
- result = reference_pipeline_result("Hey #{reference}", author: other_user)
- expect(result[:references][:user]).to eq []
- end
+ expect(doc.css('a').first.has_attribute?('data-author')).to eq(false)
end
end
@@ -83,11 +77,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-user')
expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Hey #{reference}")
- expect(result[:references][:user]).to eq [user]
- end
end
context 'mentioning a group' do
@@ -106,11 +95,6 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-group')
expect(link.attr('data-group')).to eq group.id.to_s
end
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Hey #{reference}")
- expect(result[:references][:user]).to eq group.users
- end
end
it 'links with adjacent text' do
@@ -151,10 +135,24 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(link).to have_attribute('data-user')
expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
end
+ end
+
+ describe '#namespaces' do
+ it 'returns a Hash containing all Namespaces' do
+ document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
+ filter = described_class.new(document, project: project)
+ ns = user.namespace
+
+ expect(filter.namespaces).to eq({ ns.path => ns })
+ end
+ end
+
+ describe '#usernames' do
+ it 'returns the usernames mentioned in a document' do
+ document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
+ filter = described_class.new(document, project: project)
- it 'adds to the results hash' do
- result = reference_pipeline_result("Hey #{reference}")
- expect(result[:references][:user]).to eq [user]
+ expect(filter.usernames).to eq([user.username])
end
end
end
diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
deleted file mode 100644
index 185abbb2108..00000000000
--- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::Filter::WikiLinkFilter, lib: true do
- include FilterSpecHelper
-
- let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") }
- let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) }
- let(:user) { double }
- let(:project_wiki) { ProjectWiki.new(project, user) }
-
- describe "links within the wiki (relative)" do
- describe "hierarchical links to the current directory" do
- it "doesn't rewrite non-file links" do
- link = "<a href='./page'>Link to Page</a>"
- filtered_link = filter(link, project_wiki: project_wiki).children[0]
-
- expect(filtered_link.attribute('href').value).to eq('./page')
- end
-
- it "doesn't rewrite file links" do
- link = "<a href='./page.md'>Link to Page</a>"
- filtered_link = filter(link, project_wiki: project_wiki).children[0]
-
- expect(filtered_link.attribute('href').value).to eq('./page.md')
- end
- end
-
- describe "hierarchical links to the parent directory" do
- it "doesn't rewrite non-file links" do
- link = "<a href='../page'>Link to Page</a>"
- filtered_link = filter(link, project_wiki: project_wiki).children[0]
-
- expect(filtered_link.attribute('href').value).to eq('../page')
- end
-
- it "doesn't rewrite file links" do
- link = "<a href='../page.md'>Link to Page</a>"
- filtered_link = filter(link, project_wiki: project_wiki).children[0]
-
- expect(filtered_link.attribute('href').value).to eq('../page.md')
- end
- end
-
- describe "hierarchical links to a sub-directory" do
- it "doesn't rewrite non-file links" do
- link = "<a href='./subdirectory/page'>Link to Page</a>"
- filtered_link = filter(link, project_wiki: project_wiki).children[0]
-
- expect(filtered_link.attribute('href').value).to eq('./subdirectory/page')
- end
-
- it "doesn't rewrite file links" do
- link = "<a href='./subdirectory/page.md'>Link to Page</a>"
- filtered_link = filter(link, project_wiki: project_wiki).children[0]
-
- expect(filtered_link.attribute('href').value).to eq('./subdirectory/page.md')
- end
- end
-
- describe "non-hierarchical links" do
- it 'rewrites non-file links to be at the scope of the wiki root' do
- link = "<a href='page'>Link to Page</a>"
- filtered_link = filter(link, project_wiki: project_wiki).children[0]
-
- expect(filtered_link.attribute('href').value).to match('/wiki_link_ns/wiki_link_project/wikis/page')
- end
-
- it "doesn't rewrite file links" do
- link = "<a href='page.md'>Link to Page</a>"
- filtered_link = filter(link, project_wiki: project_wiki).children[0]
-
- expect(filtered_link.attribute('href').value).to eq('page.md')
- end
- end
- end
-
- describe "links outside the wiki (absolute)" do
- it "doesn't rewrite links" do
- link = "<a href='http://example.com/page'>Link to Page</a>"
- filtered_link = filter(link, project_wiki: project_wiki).children[0]
-
- expect(filtered_link.attribute('href').value).to eq('http://example.com/page')
- end
- end
-end
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 7aa1b4a3bf6..72bc6a0b704 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -50,4 +50,112 @@ describe Banzai::Pipeline::WikiPipeline do
end
end
end
+
+ describe "Links" do
+ let(:namespace) { create(:namespace, name: "wiki_link_ns") }
+ let(:project) { create(:empty_project, :public, name: "wiki_link_project", namespace: namespace) }
+ let(:project_wiki) { ProjectWiki.new(project, double(:user)) }
+ let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
+
+ { "when GitLab is hosted at a root URL" => '/',
+ "when GitLab is hosted at a relative URL" => '/nested/relative/gitlab' }.each do |test_name, relative_url_root|
+
+ context test_name do
+ before do
+ allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return(relative_url_root)
+ end
+
+ describe "linking to pages within the wiki" do
+ context "when creating hierarchical links to the current directory" do
+ it "rewrites non-file links to be at the scope of the current directory" do
+ markdown = "[Page](./page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page\"")
+ end
+
+ it "rewrites file links to be at the scope of the current directory" do
+ markdown = "[Link to Page](./page.md)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
+ end
+ end
+
+ context "when creating hierarchical links to the parent directory" do
+ it "rewrites non-file links to be at the scope of the parent directory" do
+ markdown = "[Link to Page](../page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page\"")
+ end
+
+ it "rewrites file links to be at the scope of the parent directory" do
+ markdown = "[Link to Page](../page.md)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page.md\"")
+ end
+ end
+
+ context "when creating hierarchical links to a sub-directory" do
+ it "rewrites non-file links to be at the scope of the sub-directory" do
+ markdown = "[Link to Page](./subdirectory/page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page\"")
+ end
+
+ it "rewrites file links to be at the scope of the sub-directory" do
+ markdown = "[Link to Page](./subdirectory/page.md)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page.md\"")
+ end
+ end
+
+ describe "when creating non-hierarchical links" do
+ it 'rewrites non-file links to be at the scope of the wiki root' do
+ markdown = "[Link to Page](page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"")
+ end
+
+ it "rewrites file links to be at the scope of the current directory" do
+ markdown = "[Link to Page](page.md)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"")
+ end
+ end
+
+ describe "when creating root links" do
+ it 'rewrites non-file links to be at the scope of the wiki root' do
+ markdown = "[Link to Page](/page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"")
+ end
+
+ it 'rewrites file links to be at the scope of the wiki root' do
+ markdown = "[Link to Page](/page.md)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page.md\"")
+ end
+ end
+ end
+
+ describe "linking to pages outside the wiki (absolute)" do
+ it "doesn't rewrite links" do
+ markdown = "[Link to Page](http://example.com/page)"
+ output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+ expect(output).to include('href="http://example.com/page"')
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
new file mode 100644
index 00000000000..543b4786d84
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -0,0 +1,237 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::BaseParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+
+ subject do
+ klass = Class.new(described_class) do
+ self.reference_type = :foo
+ end
+
+ klass.new(project, user)
+ end
+
+ describe '.reference_type=' do
+ it 'sets the reference type' do
+ dummy = Class.new(described_class)
+ dummy.reference_type = :foo
+
+ expect(dummy.reference_type).to eq(:foo)
+ end
+ end
+
+ describe '#nodes_visible_to_user' do
+ let(:link) { empty_html_link }
+
+ context 'when the link has a data-project attribute' do
+ it 'returns the nodes if the attribute value equals the current project ID' do
+ link['data-project'] = project.id.to_s
+
+ expect(Ability.abilities).not_to receive(:allowed?)
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns the nodes if the user can read the project' do
+ other_project = create(:empty_project, :public)
+
+ link['data-project'] = other_project.id.to_s
+
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_project, other_project).
+ and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns an empty Array when the attribute value is empty' do
+ link['data-project'] = ''
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+
+ it 'returns an empty Array when the user can not read the project' do
+ other_project = create(:empty_project, :public)
+
+ link['data-project'] = other_project.id.to_s
+
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_project, other_project).
+ and_return(false)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-project attribute' do
+ it 'returns the nodes' do
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+ end
+
+ describe '#nodes_user_can_reference' do
+ it 'returns the nodes' do
+ link = double(:link)
+
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
+ end
+ end
+
+ describe '#referenced_by' do
+ context 'when references_relation is implemented' do
+ it 'returns a collection of objects' do
+ links = Nokogiri::HTML.fragment("<a data-foo='#{user.id}'></a>").
+ children
+
+ expect(subject).to receive(:references_relation).and_return(User)
+ expect(subject.referenced_by(links)).to eq([user])
+ end
+ end
+
+ context 'when references_relation is not implemented' do
+ it 'raises NotImplementedError' do
+ links = Nokogiri::HTML.fragment('<a data-foo="1"></a>').children
+
+ expect { subject.referenced_by(links) }.
+ to raise_error(NotImplementedError)
+ end
+ end
+ end
+
+ describe '#references_relation' do
+ it 'raises NotImplementedError' do
+ expect { subject.references_relation }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#gather_attributes_per_project' do
+ it 'returns a Hash containing attribute values per project' do
+ link = Nokogiri::HTML.fragment('<a data-project="1" data-foo="2"></a>').
+ children[0]
+
+ hash = subject.gather_attributes_per_project([link], 'data-foo')
+
+ expect(hash).to be_an_instance_of(Hash)
+
+ expect(hash[1].to_a).to eq(['2'])
+ end
+ end
+
+ describe '#grouped_objects_for_nodes' do
+ it 'returns a Hash grouping objects per ID' do
+ nodes = [double(:node)]
+
+ expect(subject).to receive(:unique_attribute_values).
+ with(nodes, 'data-user').
+ and_return([user.id])
+
+ hash = subject.grouped_objects_for_nodes(nodes, User, 'data-user')
+
+ expect(hash).to eq({ user.id => user })
+ end
+
+ it 'returns an empty Hash when the list of nodes is empty' do
+ expect(subject.grouped_objects_for_nodes([], User, 'data-user')).to eq({})
+ end
+ end
+
+ describe '#unique_attribute_values' do
+ it 'returns an Array of unique values' do
+ link = double(:link)
+
+ expect(link).to receive(:has_attribute?).
+ with('data-foo').
+ twice.
+ and_return(true)
+
+ expect(link).to receive(:attr).
+ with('data-foo').
+ twice.
+ and_return('1')
+
+ nodes = [link, link]
+
+ expect(subject.unique_attribute_values(nodes, 'data-foo')).to eq(['1'])
+ end
+ end
+
+ describe '#process' do
+ it 'gathers the references for every node matching the reference type' do
+ dummy = Class.new(described_class) do
+ self.reference_type = :test
+ end
+
+ instance = dummy.new(project, user)
+ document = Nokogiri::HTML.fragment('<a class="gfm"></a><a class="gfm" data-reference-type="test"></a>')
+
+ expect(instance).to receive(:gather_references).
+ with([document.children[1]]).
+ and_return([user])
+
+ expect(instance.process([document])).to eq([user])
+ end
+ end
+
+ describe '#gather_references' do
+ let(:link) { double(:link) }
+
+ it 'does not process links a user can not reference' do
+ expect(subject).to receive(:nodes_user_can_reference).
+ with(user, [link]).
+ and_return([])
+
+ expect(subject).to receive(:referenced_by).with([])
+
+ subject.gather_references([link])
+ end
+
+ it 'does not process links a user can not see' do
+ expect(subject).to receive(:nodes_user_can_reference).
+ with(user, [link]).
+ and_return([link])
+
+ expect(subject).to receive(:nodes_visible_to_user).
+ with(user, [link]).
+ and_return([])
+
+ expect(subject).to receive(:referenced_by).with([])
+
+ subject.gather_references([link])
+ end
+
+ it 'returns the references if a user can reference and see a link' do
+ expect(subject).to receive(:nodes_user_can_reference).
+ with(user, [link]).
+ and_return([link])
+
+ expect(subject).to receive(:nodes_visible_to_user).
+ with(user, [link]).
+ and_return([link])
+
+ expect(subject).to receive(:referenced_by).with([link])
+
+ subject.gather_references([link])
+ end
+ end
+
+ describe '#can?' do
+ it 'delegates the permissions check to the Ability class' do
+ user = double(:user)
+
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_project, project)
+
+ subject.can?(user, :read_project, project)
+ end
+ end
+
+ describe '#find_projects_for_hash_keys' do
+ it 'returns a list of Projects' do
+ expect(subject.find_projects_for_hash_keys(project.id => project)).
+ to eq([project])
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
new file mode 100644
index 00000000000..0b76d29fce0
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::CommitParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ context 'when the link has a data-project attribute' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ context 'when the link has a data-commit attribute' do
+ before do
+ link['data-commit'] = '123'
+ end
+
+ it 'returns an Array of commits' do
+ commit = double(:commit)
+
+ allow_any_instance_of(Project).to receive(:valid_repo?).
+ and_return(true)
+
+ expect(subject).to receive(:find_commits).
+ with(project, ['123']).
+ and_return([commit])
+
+ expect(subject.referenced_by([link])).to eq([commit])
+ end
+
+ it 'returns an empty Array when the commit could not be found' do
+ allow_any_instance_of(Project).to receive(:valid_repo?).
+ and_return(true)
+
+ expect(subject).to receive(:find_commits).
+ with(project, ['123']).
+ and_return([])
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+
+ it 'skips projects without valid repositories' do
+ allow_any_instance_of(Project).to receive(:valid_repo?).
+ and_return(false)
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-commit attribute' do
+ it 'returns an empty Array' do
+ allow_any_instance_of(Project).to receive(:valid_repo?).
+ and_return(true)
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link does not have a data-project attribute' do
+ it 'returns an empty Array' do
+ allow_any_instance_of(Project).to receive(:valid_repo?).
+ and_return(true)
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ describe '#commit_ids_per_project' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ it 'returns a Hash containing commit IDs per project' do
+ link['data-commit'] = '123'
+
+ hash = subject.commit_ids_per_project([link])
+
+ expect(hash).to be_an_instance_of(Hash)
+
+ expect(hash[project.id].to_a).to eq(['123'])
+ end
+
+ it 'does not add a project when the data-commit attribute is empty' do
+ hash = subject.commit_ids_per_project([link])
+
+ expect(hash).to be_empty
+ end
+ end
+
+ describe '#find_commits' do
+ it 'returns an Array of commit objects' do
+ commit = double(:commit)
+
+ expect(project).to receive(:commit).with('123').and_return(commit)
+ expect(project).to receive(:valid_repo?).and_return(true)
+
+ expect(subject.find_commits(project, %w{123})).to eq([commit])
+ end
+
+ it 'skips commit IDs for which no commit could be found' do
+ expect(project).to receive(:commit).with('123').and_return(nil)
+ expect(project).to receive(:valid_repo?).and_return(true)
+
+ expect(subject.find_commits(project, %w{123})).to eq([])
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
new file mode 100644
index 00000000000..ba982f38542
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::CommitRangeParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ context 'when the link has a data-project attribute' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ context 'when the link as a data-commit-range attribute' do
+ before do
+ link['data-commit-range'] = '123..456'
+ end
+
+ it 'returns an Array of commit ranges' do
+ range = double(:range)
+
+ expect(subject).to receive(:find_object).
+ with(project, '123..456').
+ and_return(range)
+
+ expect(subject.referenced_by([link])).to eq([range])
+ end
+
+ it 'returns an empty Array when the commit range could not be found' do
+ expect(subject).to receive(:find_object).
+ with(project, '123..456').
+ and_return(nil)
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-commit-range attribute' do
+ it 'returns an empty Array' do
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link does not have a data-project attribute' do
+ it 'returns an empty Array' do
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ describe '#commit_range_ids_per_project' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ it 'returns a Hash containing range IDs per project' do
+ link['data-commit-range'] = '123..456'
+
+ hash = subject.commit_range_ids_per_project([link])
+
+ expect(hash).to be_an_instance_of(Hash)
+
+ expect(hash[project.id].to_a).to eq(['123..456'])
+ end
+
+ it 'does not add a project when the data-commit-range attribute is empty' do
+ hash = subject.commit_range_ids_per_project([link])
+
+ expect(hash).to be_empty
+ end
+ end
+
+ describe '#find_ranges' do
+ it 'returns an Array of range objects' do
+ range = double(:commit)
+
+ expect(subject).to receive(:find_object).
+ with(project, '123..456').
+ and_return(range)
+
+ expect(subject.find_ranges(project, ['123..456'])).to eq([range])
+ end
+
+ it 'skips ranges that could not be found' do
+ expect(subject).to receive(:find_object).
+ with(project, '123..456').
+ and_return(nil)
+
+ expect(subject.find_ranges(project, ['123..456'])).to eq([])
+ end
+ end
+
+ describe '#find_object' do
+ let(:range) { double(:range) }
+
+ before do
+ expect(CommitRange).to receive(:new).and_return(range)
+ end
+
+ context 'when the range has valid commits' do
+ it 'returns the commit range' do
+ expect(range).to receive(:valid_commits?).and_return(true)
+
+ expect(subject.find_object(project, '123..456')).to eq(range)
+ end
+ end
+
+ context 'when the range does not have any valid commits' do
+ it 'returns nil' do
+ expect(range).to receive(:valid_commits?).and_return(false)
+
+ expect(subject.find_object(project, '123..456')).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
new file mode 100644
index 00000000000..a6ef8394fe7
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ context 'when the link has a data-project attribute' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ context 'when the link has a data-external-issue attribute' do
+ it 'returns an Array of ExternalIssue instances' do
+ link['data-external-issue'] = '123'
+
+ refs = subject.referenced_by([link])
+
+ expect(refs).to eq([ExternalIssue.new('123', project)])
+ end
+ end
+
+ context 'when the link does not have a data-external-issue attribute' do
+ it 'returns an empty Array' do
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link does not have a data-project attribute' do
+ it 'returns an empty Array' do
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ describe '#issue_ids_per_project' do
+ before do
+ link['data-project'] = project.id.to_s
+ end
+
+ it 'returns a Hash containing range IDs per project' do
+ link['data-external-issue'] = '123'
+
+ hash = subject.issue_ids_per_project([link])
+
+ expect(hash).to be_an_instance_of(Hash)
+
+ expect(hash[project.id].to_a).to eq(['123'])
+ end
+
+ it 'does not add a project when the data-external-issue attribute is empty' do
+ hash = subject.issue_ids_per_project([link])
+
+ expect(hash).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
new file mode 100644
index 00000000000..514c752546d
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::IssueParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before do
+ link['data-issue'] = issue.id.to_s
+ end
+
+ it 'returns the nodes when the user can read the issue' do
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_issue, issue).
+ and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns an empty Array when the user can not read the issue' do
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_issue, issue).
+ and_return(false)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-issue attribute' do
+ it 'returns an empty Array' do
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context 'when the project uses an external issue tracker' do
+ it 'returns all nodes' do
+ link = double(:link)
+
+ expect(project).to receive(:external_issue_tracker).and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+ end
+
+ describe '#referenced_by' do
+ context 'when the link has a data-issue attribute' do
+ context 'using an existing issue ID' do
+ before do
+ link['data-issue'] = issue.id.to_s
+ end
+
+ it 'returns an Array of issues' do
+ expect(subject.referenced_by([link])).to eq([issue])
+ end
+
+ it 'returns an empty Array when the list of nodes is empty' do
+ expect(subject.referenced_by([link])).to eq([issue])
+ expect(subject.referenced_by([])).to eq([])
+ end
+ end
+ end
+ end
+
+ describe '#issues_for_nodes' do
+ it 'returns a Hash containing the issues for a list of nodes' do
+ link['data-issue'] = issue.id.to_s
+ nodes = [link]
+
+ expect(subject.issues_for_nodes(nodes)).to eq({ issue.id => issue })
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb
new file mode 100644
index 00000000000..77fda47f0e7
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::LabelParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:label) { create(:label, project: project) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ describe 'when the link has a data-label attribute' do
+ context 'using an existing label ID' do
+ it 'returns an Array of labels' do
+ link['data-label'] = label.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([label])
+ end
+ end
+
+ context 'using a non-existing label ID' do
+ it 'returns an empty Array' do
+ link['data-label'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
new file mode 100644
index 00000000000..cf89ad598ea
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MergeRequestParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ subject { described_class.new(merge_request.target_project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ describe 'when the link has a data-merge-request attribute' do
+ context 'using an existing merge request ID' do
+ it 'returns an Array of merge requests' do
+ link['data-merge-request'] = merge_request.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([merge_request])
+ end
+ end
+
+ context 'using a non-existing merge request ID' do
+ it 'returns an empty Array' do
+ link['data-merge-request'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
new file mode 100644
index 00000000000..6aa45a22cc4
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::MilestoneParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:milestone) { create(:milestone, project: project) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ describe 'when the link has a data-milestone attribute' do
+ context 'using an existing milestone ID' do
+ it 'returns an Array of milestones' do
+ link['data-milestone'] = milestone.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([milestone])
+ end
+ end
+
+ context 'using a non-existing milestone ID' do
+ it 'returns an empty Array' do
+ link['data-milestone'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
new file mode 100644
index 00000000000..59127b7c5d1
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::SnippetParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:snippet) { create(:snippet, project: project) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ describe 'when the link has a data-snippet attribute' do
+ context 'using an existing snippet ID' do
+ it 'returns an Array of snippets' do
+ link['data-snippet'] = snippet.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([snippet])
+ end
+ end
+
+ context 'using a non-existing snippet ID' do
+ it 'returns an empty Array' do
+ link['data-snippet'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
new file mode 100644
index 00000000000..9a82891297d
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -0,0 +1,189 @@
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::UserParser, lib: true do
+ include ReferenceParserHelpers
+
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public, group: group, creator: user) }
+ subject { described_class.new(project, user) }
+ let(:link) { empty_html_link }
+
+ describe '#referenced_by' do
+ context 'when the link has a data-group attribute' do
+ context 'using an existing group ID' do
+ before do
+ link['data-group'] = project.group.id.to_s
+ end
+
+ it 'returns the users of the group' do
+ create(:group_member, group: group, user: user)
+
+ expect(subject.referenced_by([link])).to eq([user])
+ end
+
+ it 'returns an empty Array when the group has no users' do
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+
+ context 'using a non-existing group ID' do
+ it 'returns an empty Array' do
+ link['data-group'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+
+ context 'when the link has a data-user attribute' do
+ it 'returns an Array of users' do
+ link['data-user'] = user.id.to_s
+
+ expect(subject.referenced_by([link])).to eq([user])
+ end
+ end
+
+ context 'when the link has a data-project attribute' do
+ context 'using an existing project ID' do
+ let(:contributor) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ project.team << [contributor, :developer]
+ end
+
+ it 'returns the members of a project' do
+ link['data-project'] = project.id.to_s
+
+ # This uses an explicit sort to make sure this spec doesn't randomly
+ # fail when objects are returned in a different order.
+ refs = subject.referenced_by([link]).sort_by(&:id)
+
+ expect(refs).to eq([user, contributor])
+ end
+ end
+
+ context 'using a non-existing project ID' do
+ it 'returns an empty Array' do
+ link['data-project'] = ''
+
+ expect(subject.referenced_by([link])).to eq([])
+ end
+ end
+ end
+ end
+
+ describe '#nodes_visible_to_use?' do
+ context 'when the link has a data-group attribute' do
+ context 'using an existing group ID' do
+ before do
+ link['data-group'] = group.id.to_s
+ end
+
+ it 'returns the nodes if the user can read the group' do
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_group, group).
+ and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns an empty Array if the user can not read the group' do
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_group, group).
+ and_return(false)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-group attribute' do
+ context 'with a data-project attribute' do
+ it 'returns the nodes if the attribute value equals the current project ID' do
+ link['data-project'] = project.id.to_s
+
+ expect(Ability.abilities).not_to receive(:allowed?)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns the nodes if the user can read the project' do
+ other_project = create(:empty_project, :public)
+
+ link['data-project'] = other_project.id.to_s
+
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_project, other_project).
+ and_return(true)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+
+ it 'returns an empty Array if the user can not read the project' do
+ other_project = create(:empty_project, :public)
+
+ link['data-project'] = other_project.id.to_s
+
+ expect(Ability.abilities).to receive(:allowed?).
+ with(user, :read_project, other_project).
+ and_return(false)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context 'without a data-project attribute' do
+ it 'returns the nodes' do
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+ end
+ end
+ end
+
+ describe '#nodes_user_can_reference' do
+ context 'when the link has a data-author attribute' do
+ it 'returns the nodes when the user is a member of the project' do
+ other_project = create(:project)
+ other_project.team << [user, :developer]
+
+ link['data-project'] = other_project.id.to_s
+ link['data-author'] = user.id.to_s
+
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
+ end
+
+ it 'returns an empty Array when the project could not be found' do
+ link['data-project'] = ''
+ link['data-author'] = user.id.to_s
+
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([])
+ end
+
+ it 'returns an empty Array when the user could not be found' do
+ other_project = create(:project)
+
+ link['data-project'] = other_project.id.to_s
+ link['data-author'] = ''
+
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([])
+ end
+
+ it 'returns an empty Array when the user is not a team member' do
+ other_project = create(:project)
+
+ link['data-project'] = other_project.id.to_s
+ link['data-author'] = user.id.to_s
+
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([])
+ end
+ end
+
+ context 'when the link does not have a data-author attribute' do
+ it 'returns the nodes' do
+ expect(subject.nodes_user_can_reference(user, [link])).to eq([link])
+ end
+ end
+ end
+end
diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb
index 04afbd06929..898f1e84ab0 100644
--- a/spec/lib/ci/ansi2html_spec.rb
+++ b/spec/lib/ci/ansi2html_spec.rb
@@ -175,5 +175,14 @@ describe Ci::Ansi2html, lib: true do
it_behaves_like 'stateable converter'
end
+
+ context 'with new line' do
+ let(:pre_text) { "Hello\r" }
+ let(:pre_html) { "Hello\r" }
+ let(:text) { "\nWorld" }
+ let(:html) { "<br>World" }
+
+ it_behaves_like 'stateable converter'
+ end
end
end
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 50a77308cde..9c6b4ea5086 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -4,13 +4,20 @@ describe Ci::Charts, lib: true do
context "build_times" do
before do
- @commit = FactoryGirl.create(:ci_commit)
- FactoryGirl.create(:ci_build, commit: @commit)
+ @pipeline = FactoryGirl.create(:ci_pipeline)
+ FactoryGirl.create(:ci_build, pipeline: @pipeline)
end
it 'should return build times in minutes' do
- chart = Ci::Charts::BuildTime.new(@commit.project)
+ chart = Ci::Charts::BuildTime.new(@pipeline.project)
expect(chart.build_times).to eq([2])
end
+
+ it 'should handle nil build times' do
+ create(:ci_pipeline, duration: nil, project: @pipeline.project)
+
+ chart = Ci::Charts::BuildTime.new(@pipeline.project)
+ expect(chart.build_times).to eq([2, 0])
+ end
end
end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 9eef8ea0976..5e1d2b8e4f5 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -501,6 +501,7 @@ module Ci
})
config_processor = GitlabCiYamlProcessor.new(config, path)
+
builds = config_processor.builds_for_stage_and_ref("test", "master")
expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq(when_state)
@@ -572,7 +573,12 @@ module Ci
services: ["mysql"],
before_script: ["pwd"],
rspec: {
- artifacts: { paths: ["logs/", "binaries/"], untracked: true, name: "custom_name" },
+ artifacts: {
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ name: "custom_name",
+ expire_in: "7d"
+ },
script: "rspec"
}
})
@@ -594,13 +600,31 @@ module Ci
artifacts: {
name: "custom_name",
paths: ["logs/", "binaries/"],
- untracked: true
+ untracked: true,
+ expire_in: "7d"
}
},
when: "on_success",
allow_failure: false
})
end
+
+ %w[on_success on_failure always].each do |when_state|
+ it "returns artifacts for when #{when_state} defined" do
+ config = YAML.dump({
+ rspec: {
+ script: "rspec",
+ artifacts: { paths: ["logs/", "binaries/"], when: when_state }
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ builds = config_processor.builds_for_stage_and_ref("test", "master")
+ expect(builds.size).to eq(1)
+ expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
+ end
+ end
end
describe "Dependencies" do
@@ -619,19 +643,19 @@ module Ci
context 'no dependencies' do
let(:dependencies) { }
- it { expect { subject }.to_not raise_error }
+ it { expect { subject }.not_to raise_error }
end
context 'dependencies to builds' do
let(:dependencies) { ['build1', 'build2'] }
- it { expect { subject }.to_not raise_error }
+ it { expect { subject }.not_to raise_error }
end
context 'dependencies to builds defined as symbols' do
let(:dependencies) { [:build1, :build2] }
- it { expect { subject }.to_not raise_error }
+ it { expect { subject }.not_to raise_error }
end
context 'undefined dependency' do
@@ -967,6 +991,27 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string")
end
+ it "returns errors if job artifacts:when is not an a predefined value" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:when parameter should be on_success, on_failure or always")
+ end
+
+ it "returns errors if job artifacts:expire_in is not an a string" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration")
+ end
+
+ it "returns errors if job artifacts:expire_in is not an a valid duration" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration")
+ end
+
it "returns errors if job artifacts:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb
new file mode 100644
index 00000000000..4d8cb787dde
--- /dev/null
+++ b/spec/lib/container_registry/blob_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe ContainerRegistry::Blob do
+ let(:digest) { 'sha256:0123456789012345' }
+ let(:config) do
+ {
+ 'digest' => digest,
+ 'mediaType' => 'binary',
+ 'size' => 1000
+ }
+ end
+
+ let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
+ let(:repository) { registry.repository('group/test') }
+ let(:blob) { repository.blob(config) }
+
+ it { expect(blob).to respond_to(:repository) }
+ it { expect(blob).to delegate_method(:registry).to(:repository) }
+ it { expect(blob).to delegate_method(:client).to(:repository) }
+
+ context '#path' do
+ subject { blob.path }
+
+ it { is_expected.to eq('example.com/group/test@sha256:0123456789012345') }
+ end
+
+ context '#digest' do
+ subject { blob.digest }
+
+ it { is_expected.to eq(digest) }
+ end
+
+ context '#type' do
+ subject { blob.type }
+
+ it { is_expected.to eq('binary') }
+ end
+
+ context '#revision' do
+ subject { blob.revision }
+
+ it { is_expected.to eq('0123456789012345') }
+ end
+
+ context '#short_revision' do
+ subject { blob.short_revision }
+
+ it { is_expected.to eq('012345678') }
+ end
+
+ context '#delete' do
+ before do
+ stub_request(:delete, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
+ to_return(status: 200)
+ end
+
+ subject { blob.delete }
+
+ it { is_expected.to be_truthy }
+ end
+end
diff --git a/spec/lib/container_registry/registry_spec.rb b/spec/lib/container_registry/registry_spec.rb
new file mode 100644
index 00000000000..4f3f8b24fc4
--- /dev/null
+++ b/spec/lib/container_registry/registry_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe ContainerRegistry::Registry do
+ let(:path) { nil }
+ let(:registry) { described_class.new('http://example.com', path: path) }
+
+ subject { registry }
+
+ it { is_expected.to respond_to(:client) }
+ it { is_expected.to respond_to(:uri) }
+ it { is_expected.to respond_to(:path) }
+
+ it { expect(subject.repository('test')).not_to be_nil }
+
+ context '#path' do
+ subject { registry.path }
+
+ context 'path from URL' do
+ it { is_expected.to eq('example.com') }
+ end
+
+ context 'custom path' do
+ let(:path) { 'registry.example.com' }
+
+ it { is_expected.to eq(path) }
+ end
+ end
+end
diff --git a/spec/lib/container_registry/repository_spec.rb b/spec/lib/container_registry/repository_spec.rb
new file mode 100644
index 00000000000..279709521c9
--- /dev/null
+++ b/spec/lib/container_registry/repository_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe ContainerRegistry::Repository do
+ let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
+ let(:repository) { registry.repository('group/test') }
+
+ it { expect(repository).to respond_to(:registry) }
+ it { expect(repository).to delegate_method(:client).to(:registry) }
+ it { expect(repository.tag('test')).not_to be_nil }
+
+ context '#path' do
+ subject { repository.path }
+
+ it { is_expected.to eq('example.com/group/test') }
+ end
+
+ context 'manifest processing' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/tags/list').
+ with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }).
+ to_return(
+ status: 200,
+ body: JSON.dump(tags: ['test']),
+ headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' })
+ end
+
+ context '#manifest' do
+ subject { repository.manifest }
+
+ it { is_expected.not_to be_nil }
+ end
+
+ context '#valid?' do
+ subject { repository.valid? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context '#tags' do
+ subject { repository.tags }
+
+ it { is_expected.not_to be_empty }
+ end
+ end
+
+ context '#delete_tags' do
+ let(:tag) { ContainerRegistry::Tag.new(repository, 'tag') }
+
+ before { expect(repository).to receive(:tags).twice.and_return([tag]) }
+
+ subject { repository.delete_tags }
+
+ context 'succeeds' do
+ before { expect(tag).to receive(:delete).and_return(true) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'any fails' do
+ before { expect(tag).to receive(:delete).and_return(false) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
new file mode 100644
index 00000000000..858cb0bb134
--- /dev/null
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe ContainerRegistry::Tag do
+ let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
+ let(:repository) { registry.repository('group/test') }
+ let(:tag) { repository.tag('tag') }
+ let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } }
+
+ it { expect(tag).to respond_to(:repository) }
+ it { expect(tag).to delegate_method(:registry).to(:repository) }
+ it { expect(tag).to delegate_method(:client).to(:repository) }
+
+ context '#path' do
+ subject { tag.path }
+
+ it { is_expected.to eq('example.com/group/test:tag') }
+ end
+
+ context 'manifest processing' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/manifests/tag').
+ with(headers: headers).
+ to_return(
+ status: 200,
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'),
+ headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' })
+ end
+
+ context '#layers' do
+ subject { tag.layers }
+
+ it { expect(subject.length).to eq(1) }
+ end
+
+ context '#total_size' do
+ subject { tag.total_size }
+
+ it { is_expected.to eq(2319870) }
+ end
+
+ context 'config processing' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
+ with(headers: { 'Accept' => 'application/octet-stream' }).
+ to_return(
+ status: 200,
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json'))
+ end
+
+ context '#config' do
+ subject { tag.config }
+
+ it { is_expected.not_to be_nil }
+ end
+
+ context '#created_at' do
+ subject { tag.created_at }
+
+ it { is_expected.not_to be_nil }
+ end
+ end
+ end
+
+ context 'manifest digest' do
+ before do
+ stub_request(:head, 'http://example.com/v2/group/test/manifests/tag').
+ with(headers: headers).
+ to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' })
+ end
+
+ context '#digest' do
+ subject { tag.digest }
+
+ it { is_expected.to eq('sha256:digest') }
+ end
+
+ context '#delete' do
+ before do
+ stub_request(:delete, 'http://example.com/v2/group/test/manifests/sha256:digest').
+ with(headers: headers).
+ to_return(status: 200)
+ end
+
+ subject { tag.delete }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+end
diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb
index c2a7b20b84d..309a88151cf 100644
--- a/spec/lib/disable_email_interceptor_spec.rb
+++ b/spec/lib/disable_email_interceptor_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe DisableEmailInterceptor, lib: true do
before do
- ActionMailer::Base.register_interceptor(DisableEmailInterceptor)
+ Mail.register_interceptor(DisableEmailInterceptor)
end
it 'should not send emails' do
@@ -14,7 +14,7 @@ describe DisableEmailInterceptor, lib: true do
# Removing interceptor from the list because unregister_interceptor is
# implemented in later version of mail gem
# See: https://github.com/mikel/mail/pull/705
- Mail.class_variable_set(:@@delivery_interceptors, [])
+ Mail.unregister_interceptor(DisableEmailInterceptor)
end
def deliver_mail
diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb
index 53f5d6c5c80..88a71528867 100644
--- a/spec/lib/gitlab/akismet_helper_spec.rb
+++ b/spec/lib/gitlab/akismet_helper_spec.rb
@@ -6,8 +6,8 @@ describe Gitlab::AkismetHelper, type: :helper do
before do
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- current_application_settings.akismet_enabled = true
- current_application_settings.akismet_api_key = '12345'
+ allow_any_instance_of(ApplicationSetting).to receive(:akismet_enabled).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:akismet_api_key).and_return('12345')
end
describe '#check_for_spam?' do
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index aad291c03cd..7bec1367156 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -1,9 +1,47 @@
require 'spec_helper'
describe Gitlab::Auth, lib: true do
- let(:gl_auth) { Gitlab::Auth.new }
+ let(:gl_auth) { described_class }
- describe :find do
+ describe 'find_for_git_client' do
+ it 'recognizes CI' do
+ token = '123'
+ project = create(:empty_project)
+ project.update_attributes(runners_token: token, builds_enabled: true)
+ ip = 'ip'
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'gitlab-ci-token')
+ expect(gl_auth.find_for_git_client('gitlab-ci-token', token, project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, :ci))
+ end
+
+ it 'recognizes master passwords' do
+ user = create(:user, password: 'password')
+ ip = 'ip'
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
+ expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :gitlab_or_ldap))
+ end
+
+ it 'recognizes OAuth tokens' do
+ user = create(:user)
+ application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+ token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+ ip = 'ip'
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
+ expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :oauth))
+ end
+
+ it 'returns double nil for invalid credentials' do
+ login = 'foo'
+ ip = 'ip'
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login)
+ expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
+ end
+ end
+
+ describe 'find_with_user_password' do
let!(:user) do
create(:user,
username: username,
@@ -14,25 +52,25 @@ describe Gitlab::Auth, lib: true do
let(:password) { 'my-secret' }
it "should find user by valid login/password" do
- expect( gl_auth.find(username, password) ).to eql user
+ expect( gl_auth.find_with_user_password(username, password) ).to eql user
end
it 'should find user by valid email/password with case-insensitive email' do
- expect(gl_auth.find(user.email.upcase, password)).to eql user
+ expect(gl_auth.find_with_user_password(user.email.upcase, password)).to eql user
end
it 'should find user by valid username/password with case-insensitive username' do
- expect(gl_auth.find(username.upcase, password)).to eql user
+ expect(gl_auth.find_with_user_password(username.upcase, password)).to eql user
end
it "should not find user with invalid password" do
password = 'wrong'
- expect( gl_auth.find(username, password) ).not_to eql user
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
it "should not find user with invalid login" do
user = 'wrong'
- expect( gl_auth.find(username, password) ).not_to eql user
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
context "with ldap enabled" do
@@ -43,13 +81,13 @@ describe Gitlab::Auth, lib: true do
it "tries to autheticate with db before ldap" do
expect(Gitlab::LDAP::Authentication).not_to receive(:login)
- gl_auth.find(username, password)
+ gl_auth.find_with_user_password(username, password)
end
it "uses ldap as fallback to for authentication" do
expect(Gitlab::LDAP::Authentication).to receive(:login)
- gl_auth.find('ldap_user', 'password')
+ gl_auth.find_with_user_password('ldap_user', 'password')
end
end
end
diff --git a/spec/lib/award_emoji_spec.rb b/spec/lib/gitlab/award_emoji_spec.rb
index 88c22912950..0f3852b1729 100644
--- a/spec/lib/award_emoji_spec.rb
+++ b/spec/lib/gitlab/award_emoji_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-describe AwardEmoji do
+describe Gitlab::AwardEmoji do
describe '.urls' do
- subject { AwardEmoji.urls }
+ subject { Gitlab::AwardEmoji.urls }
it { is_expected.to be_an_instance_of(Array) }
- it { is_expected.to_not be_empty }
+ it { is_expected.not_to be_empty }
context 'every Hash in the Array' do
it 'has the correct keys and values' do
@@ -19,7 +19,7 @@ describe AwardEmoji do
describe '.emoji_by_category' do
it "only contains known categories" do
- undefined_categories = AwardEmoji.emoji_by_category.keys - AwardEmoji::CATEGORIES.keys
+ undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys
expect(undefined_categories).to be_empty
end
end
diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb
deleted file mode 100644
index cd26dca0998..00000000000
--- a/spec/lib/gitlab/backend/grack_auth_spec.rb
+++ /dev/null
@@ -1,209 +0,0 @@
-require "spec_helper"
-
-describe Grack::Auth, lib: true do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- let(:app) { lambda { |env| [200, {}, "Success!"] } }
- let!(:auth) { Grack::Auth.new(app) }
- let(:env) do
- {
- 'rack.input' => '',
- 'REQUEST_METHOD' => 'GET',
- 'QUERY_STRING' => 'service=git-upload-pack'
- }
- end
- let(:status) { auth.call(env).first }
-
- describe "#call" do
- context "when the project doesn't exist" do
- before do
- env["PATH_INFO"] = "doesnt/exist.git"
- end
-
- context "when no authentication is provided" do
- it "responds with status 401" do
- expect(status).to eq(401)
- end
- end
-
- context "when username and password are provided" do
- context "when authentication fails" do
- before do
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope")
- end
-
- it "responds with status 401" do
- expect(status).to eq(401)
- end
- end
-
- context "when authentication succeeds" do
- before do
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
- end
-
- it "responds with status 404" do
- expect(status).to eq(404)
- end
- end
- end
- end
-
- context "when the Wiki for a project exists" do
- before do
- @wiki = ProjectWiki.new(project)
- env["PATH_INFO"] = "#{@wiki.repository.path_with_namespace}.git/info/refs"
- project.update_attribute(:visibility_level, Project::PUBLIC)
- end
-
- it "responds with the right project" do
- response = auth.call(env)
- json_body = ActiveSupport::JSON.decode(response[2][0])
-
- expect(response.first).to eq(200)
- expect(json_body['RepoPath']).to include(@wiki.repository.path_with_namespace)
- end
- end
-
- context "when the project exists" do
- before do
- env["PATH_INFO"] = project.path_with_namespace + ".git"
- end
-
- context "when the project is public" do
- before do
- project.update_attribute(:visibility_level, Project::PUBLIC)
- end
-
- it "responds with status 200" do
- expect(status).to eq(200)
- end
- end
-
- context "when the project is private" do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
-
- context "when no authentication is provided" do
- it "responds with status 401" do
- expect(status).to eq(401)
- end
- end
-
- context "when username and password are provided" do
- context "when authentication fails" do
- before do
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, "nope")
- end
-
- it "responds with status 401" do
- expect(status).to eq(401)
- end
-
- context "when the user is IP banned" do
- before do
- expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
- allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
- end
-
- it "responds with status 401" do
- expect(status).to eq(401)
- end
- end
- end
-
- context "when authentication succeeds" do
- before do
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
- end
-
- context "when the user has access to the project" do
- before do
- project.team << [user, :master]
- end
-
- context "when the user is blocked" do
- before do
- user.block
- project.team << [user, :master]
- end
-
- it "responds with status 404" do
- expect(status).to eq(404)
- end
- end
-
- context "when the user isn't blocked" do
- before do
- expect(Rack::Attack::Allow2Ban).to receive(:reset)
- end
-
- it "responds with status 200" do
- expect(status).to eq(200)
- end
- end
-
- context "when blank password attempts follow a valid login" do
- let(:options) { Gitlab.config.rack_attack.git_basic_auth }
- let(:maxretry) { options[:maxretry] - 1 }
- let(:ip) { '1.2.3.4' }
-
- before do
- allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
- Rack::Attack::Allow2Ban.reset(ip, options)
- end
-
- after do
- Rack::Attack::Allow2Ban.reset(ip, options)
- end
-
- def attempt_login(include_password)
- password = include_password ? user.password : ""
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, password)
- Grack::Auth.new(app)
- auth.call(env).first
- end
-
- it "repeated attempts followed by successful attempt" do
- maxretry.times.each do
- expect(attempt_login(false)).to eq(401)
- end
-
- expect(attempt_login(true)).to eq(200)
- expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
-
- maxretry.times.each do
- expect(attempt_login(false)).to eq(401)
- end
- end
- end
- end
-
- context "when the user doesn't have access to the project" do
- it "responds with status 404" do
- expect(status).to eq(404)
- end
- end
- end
- end
-
- context "when a gitlab ci token is provided" do
- let(:token) { "123" }
- let(:project) { FactoryGirl.create :empty_project }
-
- before do
- project.update_attributes(runners_token: token, builds_enabled: true)
-
- env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token)
- end
-
- it "responds with status 200" do
- expect(status).to eq(200)
- end
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb
index b6f7a2e7ec4..2034445a197 100644
--- a/spec/lib/gitlab/badge/build_spec.rb
+++ b/spec/lib/gitlab/badge/build_spec.rb
@@ -42,9 +42,7 @@ describe Gitlab::Badge::Build do
end
context 'build exists' do
- let(:ci_commit) { create(:ci_commit, project: project, sha: sha, ref: branch) }
- let!(:build) { create(:ci_build, commit: ci_commit) }
-
+ let!(:build) { create_build(project, sha, branch) }
context 'build success' do
before { build.success! }
@@ -96,6 +94,28 @@ describe Gitlab::Badge::Build do
end
end
+ context 'when outdated pipeline for given ref exists' do
+ before do
+ build = create_build(project, sha, branch)
+ build.success!
+
+ old_build = create_build(project, '11eeffdd', branch)
+ old_build.drop!
+ end
+
+ it 'does not take outdated pipeline into account' do
+ expect(badge.to_s).to eq 'build-success'
+ end
+ end
+
+ def create_build(project, sha, branch)
+ pipeline = create(:ci_pipeline, project: project,
+ sha: sha,
+ ref: branch)
+
+ create(:ci_build, pipeline: pipeline)
+ end
+
def status_node(data, status)
xml = Nokogiri::XML.parse(data)
xml.at(%Q{text:contains("#{status}")})
diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb
index af839f42421..760d66a1488 100644
--- a/spec/lib/gitlab/bitbucket_import/client_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb
@@ -1,12 +1,14 @@
require 'spec_helper'
describe Gitlab::BitbucketImport::Client, lib: true do
+ include ImportSpecHelper
+
let(:token) { '123456' }
let(:secret) { 'secret' }
let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) }
before do
- Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket")
+ stub_omniauth_provider('bitbucket')
end
it 'all OAuth client options are symbols' do
@@ -59,7 +61,7 @@ describe Gitlab::BitbucketImport::Client, lib: true do
bitbucket_access_token_secret: "test" } })
project.import_url = "ssh://git@bitbucket.org/test/test.git"
- expect { described_class.from_project(project) }.to_not raise_error
+ expect { described_class.from_project(project) }.not_to raise_error
end
end
end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 1a833f255a5..aa00f32becb 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -1,8 +1,10 @@
require 'spec_helper'
describe Gitlab::BitbucketImport::Importer, lib: true do
+ include ImportSpecHelper
+
before do
- Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket")
+ stub_omniauth_provider('bitbucket')
end
let(:statuses) do
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
index 46a5b7fce65..711a3e1c7d4 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
@@ -122,7 +122,7 @@ describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
describe 'empty path', path: '' do
subject { |example| path(example) }
- it { is_expected.to_not have_parent }
+ it { is_expected.not_to have_parent }
describe '#children' do
subject { |example| path(example).children }
diff --git a/spec/lib/gitlab/ci/config/loader_spec.rb b/spec/lib/gitlab/ci/config/loader_spec.rb
new file mode 100644
index 00000000000..2d44b1f60f1
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/loader_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Loader do
+ let(:loader) { described_class.new(yml) }
+
+ context 'when yaml syntax is correct' do
+ let(:yml) { 'image: ruby:2.2' }
+
+ describe '#valid?' do
+ it 'returns true' do
+ expect(loader.valid?).to be true
+ end
+ end
+
+ describe '#load!' do
+ it 'returns a valid hash' do
+ expect(loader.load!).to eq(image: 'ruby:2.2')
+ end
+ end
+ end
+
+ context 'when yaml syntax is incorrect' do
+ let(:yml) { '// incorrect' }
+
+ describe '#valid?' do
+ it 'returns false' do
+ expect(loader.valid?).to be false
+ end
+ end
+
+ describe '#load!' do
+ it 'raises error' do
+ expect { loader.load! }.to raise_error(
+ Gitlab::Ci::Config::Loader::FormatError,
+ 'Invalid configuration format'
+ )
+ end
+ end
+ end
+
+ context 'when yaml config is empty' do
+ let(:yml) { '' }
+
+ describe '#valid?' do
+ it 'returns false' do
+ expect(loader.valid?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/configurable_spec.rb b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
new file mode 100644
index 00000000000..47c68f96dc8
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Configurable do
+ let(:node) { Class.new }
+
+ before do
+ node.include(described_class)
+ end
+
+ describe 'allowed nodes' do
+ before do
+ node.class_eval do
+ allow_node :object, Object, description: 'test object'
+ end
+ end
+
+ describe '#allowed_nodes' do
+ it 'has valid allowed nodes' do
+ expect(node.allowed_nodes).to include :object
+ end
+
+ it 'creates a node factory' do
+ expect(node.allowed_nodes[:object])
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Factory
+ end
+
+ it 'returns a duplicated factory object' do
+ first_factory = node.allowed_nodes[:object]
+ second_factory = node.allowed_nodes[:object]
+
+ expect(first_factory).not_to be_equal(second_factory)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb
new file mode 100644
index 00000000000..d681aa32456
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Factory do
+ describe '#create!' do
+ let(:factory) { described_class.new(entry_class) }
+ let(:entry_class) { Gitlab::Ci::Config::Node::Script }
+
+ context 'when value setting value' do
+ it 'creates entry with valid value' do
+ entry = factory
+ .with(value: ['ls', 'pwd'])
+ .create!
+
+ expect(entry.value).to eq "ls\npwd"
+ end
+
+ context 'when setting description' do
+ it 'creates entry with description' do
+ entry = factory
+ .with(value: ['ls', 'pwd'])
+ .with(description: 'test description')
+ .create!
+
+ expect(entry.value).to eq "ls\npwd"
+ expect(entry.description).to eq 'test description'
+ end
+ end
+ end
+
+ context 'when not setting value' do
+ it 'raises error' do
+ expect { factory.create! }.to raise_error(
+ Gitlab::Ci::Config::Node::Factory::InvalidFactory
+ )
+ end
+ end
+
+ context 'when creating a null entry' do
+ it 'creates a null entry' do
+ entry = factory
+ .with(value: nil)
+ .nullify!
+ .create!
+
+ expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Null
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb
new file mode 100644
index 00000000000..b1972172435
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Global do
+ let(:global) { described_class.new(hash) }
+
+ describe '#allowed_nodes' do
+ it 'can contain global config keys' do
+ expect(global.allowed_nodes).to include :before_script
+ end
+
+ it 'returns a hash' do
+ expect(global.allowed_nodes).to be_a Hash
+ end
+ end
+
+ context 'when hash is valid' do
+ let(:hash) do
+ { before_script: ['ls', 'pwd'] }
+ end
+
+ describe '#process!' do
+ before { global.process! }
+
+ it 'creates nodes hash' do
+ expect(global.nodes).to be_an Array
+ end
+
+ it 'creates node object for each entry' do
+ expect(global.nodes.count).to eq 1
+ end
+
+ it 'creates node object using valid class' do
+ expect(global.nodes.first)
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Script
+ end
+
+ it 'sets correct description for nodes' do
+ expect(global.nodes.first.description)
+ .to eq 'Script that will be executed before each job.'
+ end
+ end
+
+ describe '#leaf?' do
+ it 'is not leaf' do
+ expect(global).not_to be_leaf
+ end
+ end
+
+ describe '#before_script' do
+ context 'when processed' do
+ before { global.process! }
+
+ it 'returns correct script' do
+ expect(global.before_script).to eq "ls\npwd"
+ end
+ end
+
+ context 'when not processed' do
+ it 'returns nil' do
+ expect(global.before_script).to be nil
+ end
+ end
+ end
+ end
+
+ context 'when hash is not valid' do
+ before { global.process! }
+
+ let(:hash) do
+ { before_script: 'ls' }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(global).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'reports errors from child nodes' do
+ expect(global.errors)
+ .to include 'before_script should be an array of strings'
+ end
+ end
+
+ describe '#before_script' do
+ it 'raises error' do
+ expect { global.before_script }.to raise_error(
+ Gitlab::Ci::Config::Node::Entry::InvalidError
+ )
+ end
+ end
+ end
+
+ context 'when value is not a hash' do
+ let(:hash) { [] }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(global).not_to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb
new file mode 100644
index 00000000000..36101c62462
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/null_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Null do
+ let(:entry) { described_class.new(nil) }
+
+ describe '#leaf?' do
+ it 'is leaf node' do
+ expect(entry).to be_leaf
+ end
+ end
+
+ describe '#any_method' do
+ it 'responds with nil' do
+ expect(entry.any_method).to be nil
+ end
+ end
+
+ describe '#value' do
+ it 'returns nil' do
+ expect(entry.value).to be nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb
new file mode 100644
index 00000000000..e4d6481f8a5
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/script_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Script do
+ let(:entry) { described_class.new(value) }
+
+ describe '#validate!' do
+ before { entry.validate! }
+
+ context 'when entry value is correct' do
+ let(:value) { ['ls', 'pwd'] }
+
+ describe '#value' do
+ it 'returns concatenated command' do
+ expect(entry.value).to eq "ls\npwd"
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is not correct' do
+ let(:value) { 'ls' }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include /should be an array of strings/
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
new file mode 100644
index 00000000000..3871d939feb
--- /dev/null
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config do
+ let(:config) do
+ described_class.new(yml)
+ end
+
+ context 'when config is valid' do
+ let(:yml) do
+ <<-EOS
+ image: ruby:2.2
+
+ rspec:
+ script:
+ - gem install rspec
+ - rspec
+ EOS
+ end
+
+ describe '#to_hash' do
+ it 'returns hash created from string' do
+ hash = {
+ image: 'ruby:2.2',
+ rspec: {
+ script: ['gem install rspec',
+ 'rspec']
+ }
+ }
+
+ expect(config.to_hash).to eq hash
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(config).to be_valid
+ end
+
+ it 'has no errors' do
+ expect(config.errors).to be_empty
+ end
+ end
+ end
+
+ context 'when config is invalid' do
+ context 'when yml is incorrect' do
+ let(:yml) { '// invalid' }
+
+ describe '.new' do
+ it 'raises error' do
+ expect { config }.to raise_error(
+ Gitlab::Ci::Config::Loader::FormatError,
+ /Invalid configuration format/
+ )
+ end
+ end
+ end
+
+ context 'when config logic is incorrect' do
+ let(:yml) { 'before_script: "ls"' }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(config).not_to be_valid
+ end
+
+ it 'has errors' do
+ expect(config.errors).not_to be_empty
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
new file mode 100644
index 00000000000..1ec539066a7
--- /dev/null
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -0,0 +1,148 @@
+require 'spec_helper'
+
+describe Gitlab::Database::MigrationHelpers, lib: true do
+ let(:model) do
+ ActiveRecord::Migration.new.extend(
+ Gitlab::Database::MigrationHelpers
+ )
+ end
+
+ before { allow(model).to receive(:puts) }
+
+ describe '#add_concurrent_index' do
+ context 'outside a transaction' do
+ before do
+ expect(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ context 'using PostgreSQL' do
+ before { expect(Gitlab::Database).to receive(:postgresql?).and_return(true) }
+
+ it 'creates the index concurrently' do
+ expect(model).to receive(:add_index).
+ with(:users, :foo, algorithm: :concurrently)
+
+ model.add_concurrent_index(:users, :foo)
+ end
+
+ it 'creates unique index concurrently' do
+ expect(model).to receive(:add_index).
+ with(:users, :foo, { algorithm: :concurrently, unique: true })
+
+ 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)
+
+ expect(model).to receive(:add_index).
+ with(:users, :foo, {})
+
+ model.add_concurrent_index(:users, :foo)
+ end
+ end
+ end
+
+ context 'inside a transaction' do
+ it 'raises RuntimeError' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect { model.add_concurrent_index(:users, :foo) }.
+ to raise_error(RuntimeError)
+ end
+ end
+ end
+
+ describe '#update_column_in_batches' do
+ before do
+ create_list(:empty_project, 5)
+ end
+
+ it 'updates all the rows in a table' do
+ model.update_column_in_batches(:projects, :import_error, 'foo')
+
+ expect(Project.where(import_error: 'foo').count).to eq(5)
+ end
+
+ it 'updates boolean values correctly' do
+ model.update_column_in_batches(:projects, :archived, true)
+
+ expect(Project.where(archived: true).count).to eq(5)
+ end
+ end
+
+ describe '#add_column_with_default' do
+ context 'outside of a transaction' do
+ before do
+ expect(model).to receive(:transaction_open?).and_return(false)
+
+ expect(model).to receive(:transaction).twice.and_yield
+
+ expect(model).to receive(:add_column).
+ with(:projects, :foo, :integer, default: nil)
+
+ expect(model).to receive(:change_column_default).
+ with(:projects, :foo, 10)
+ end
+
+ it 'adds the column while allowing NULL values' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10)
+
+ expect(model).not_to receive(:change_column_null)
+
+ model.add_column_with_default(:projects, :foo, :integer,
+ default: 10,
+ allow_null: true)
+ end
+
+ it 'adds the column while not allowing NULL values' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10)
+
+ expect(model).to receive(:change_column_null).
+ with(:projects, :foo, false)
+
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end
+
+ it 'removes the added column whenever updating the rows fails' do
+ expect(model).to receive(:update_column_in_batches).
+ with(:projects, :foo, 10).
+ and_raise(RuntimeError)
+
+ expect(model).to receive(:remove_column).
+ with(:projects, :foo)
+
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
+
+ it 'removes the added column whenever changing a column NULL constraint fails' do
+ expect(model).to receive(:change_column_null).
+ with(:projects, :foo, false).
+ and_raise(RuntimeError)
+
+ expect(model).to receive(:remove_column).
+ with(:projects, :foo)
+
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'inside a transaction' do
+ it 'raises RuntimeError' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ model.add_column_with_default(:projects, :foo, :integer, default: 10)
+ end.to raise_error(RuntimeError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index d0a447753b7..3031559c613 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -39,6 +39,22 @@ describe Gitlab::Database, lib: true do
end
end
+ describe '.nulls_last_order' do
+ context 'when using PostgreSQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(true) }
+
+ it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column ASC NULLS LAST'}
+ it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC NULLS LAST'}
+ end
+
+ context 'when using MySQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(false) }
+
+ it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column IS NULL, column ASC'}
+ it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC'}
+ end
+ end
+
describe '#true_value' do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
index 7d6cce6daec..c19f33e2224 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -57,7 +57,7 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#diffs' do
subject { message.diffs }
- it { is_expected.to all(be_an_instance_of Gitlab::Git::Diff) }
+ it { is_expected.to all(be_an_instance_of Gitlab::Diff::File) }
end
describe '#diffs_count' do
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 0a7ca3ec848..0af249d8690 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -33,8 +33,8 @@ describe Gitlab::Gfm::ReferenceRewriter do
end
it { is_expected.to include issue_first.to_reference(new_project) }
- it { is_expected.to_not include issue_second.to_reference(new_project) }
- it { is_expected.to_not include merge_request.to_reference(new_project) }
+ it { is_expected.not_to include issue_second.to_reference(new_project) }
+ it { is_expected.not_to include merge_request.to_reference(new_project) }
end
context 'description ambigous elements' do
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index eda956e6f0a..6eca33f9fee 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -32,13 +32,13 @@ describe Gitlab::Gfm::UploadsRewriter do
let(:new_paths) { new_files.map(&:path) }
it 'rewrites content' do
- expect(new_text).to_not eq text
+ expect(new_text).not_to eq text
expect(new_text.length).to eq text.length
end
it 'copies files' do
expect(new_files).to all(exist)
- expect(old_paths).to_not match_array new_paths
+ expect(old_paths).not_to match_array new_paths
expect(old_paths).to all(include(old_project.path_with_namespace))
expect(new_paths).to all(include(new_project.path_with_namespace))
end
@@ -48,8 +48,8 @@ describe Gitlab::Gfm::UploadsRewriter do
end
it 'generates a new secret for each file' do
- expect(new_paths).to_not include image_uploader.secret
- expect(new_paths).to_not include zip_uploader.secret
+ expect(new_paths).not_to include image_uploader.secret
+ expect(new_paths).not_to include zip_uploader.secret
end
end
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
index 55e86d4ceac..9ae02a6c45f 100644
--- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
@@ -29,6 +29,7 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
commit_id: nil,
line_code: nil,
author_id: project.creator_id,
+ type: nil,
created_at: created_at,
updated_at: updated_at
}
@@ -56,6 +57,7 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_4_3',
author_id: project.creator_id,
+ type: 'LegacyDiffNote',
created_at: created_at,
updated_at: updated_at
}
diff --git a/spec/lib/gitlab/github_import/hook_formatter_spec.rb b/spec/lib/gitlab/github_import/hook_formatter_spec.rb
new file mode 100644
index 00000000000..110ba428258
--- /dev/null
+++ b/spec/lib/gitlab/github_import/hook_formatter_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::HookFormatter, lib: true do
+ describe '#id' do
+ it 'returns raw id' do
+ raw = double(id: 100000)
+ formatter = described_class.new(raw)
+ expect(formatter.id).to eq 100000
+ end
+ end
+
+ describe '#name' do
+ it 'returns raw id' do
+ raw = double(name: 'web')
+ formatter = described_class.new(raw)
+ expect(formatter.name).to eq 'web'
+ end
+ end
+
+ describe '#config' do
+ it 'returns raw config.attrs' do
+ raw = double(config: double(attrs: { url: 'http://something.com/webhook' }))
+ formatter = described_class.new(raw)
+ expect(formatter.config).to eq({ url: 'http://something.com/webhook' })
+ end
+ end
+
+ describe '#valid?' do
+ it 'returns true when events contains the wildcard event' do
+ raw = double(events: ['*', 'commit_comment'], active: true)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq true
+ end
+
+ it 'returns true when events contains the create event' do
+ raw = double(events: ['create', 'commit_comment'], active: true)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq true
+ end
+
+ it 'returns true when events contains delete event' do
+ raw = double(events: ['delete', 'commit_comment'], active: true)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq true
+ end
+
+ it 'returns true when events contains pull_request event' do
+ raw = double(events: ['pull_request', 'commit_comment'], active: true)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq true
+ end
+
+ it 'returns false when events does not contains branch related events' do
+ raw = double(events: ['member', 'commit_comment'], active: true)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq false
+ end
+
+ it 'returns false when hook is not active' do
+ raw = double(events: ['pull_request', 'commit_comment'], active: false)
+ formatter = described_class.new(raw)
+ expect(formatter.valid?).to eq false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitignore_spec.rb b/spec/lib/gitlab/gitignore_spec.rb
new file mode 100644
index 00000000000..72baa516cc4
--- /dev/null
+++ b/spec/lib/gitlab/gitignore_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Gitignore do
+ subject { Gitlab::Gitignore }
+
+ describe '.all' do
+ it 'strips the gitignore suffix' do
+ expect(subject.all.first.name).not_to end_with('.gitignore')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all.map(&:name)
+
+ expect(all).to include('Vim')
+ expect(all).to include('Ruby')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect(subject.find('mepmep-yadida')).to be nil
+ end
+
+ it 'returns the Gitignore object of a valid file' do
+ ruby = subject.find('Ruby')
+
+ expect(ruby).to be_a Gitlab::Gitignore
+ expect(ruby.name).to eq('Ruby')
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ gitignore = subject.new(Rails.root.join('vendor/gitignore/Ruby.gitignore'))
+
+ expect(gitignore.name).to eq 'Ruby'
+ expect(gitignore.content).to start_with('*.gem')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb
index e6831e7c383..cd8e805466a 100644
--- a/spec/lib/gitlab/gitlab_import/client_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/client_spec.rb
@@ -1,11 +1,13 @@
require 'spec_helper'
describe Gitlab::GitlabImport::Client, lib: true do
+ include ImportSpecHelper
+
let(:token) { '123456' }
let(:client) { Gitlab::GitlabImport::Client.new(token) }
before do
- Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "gitlab")
+ stub_omniauth_provider('gitlab')
end
it 'all OAuth2 client options are symbols' do
diff --git a/spec/lib/gitlab/import_url_spec.rb b/spec/lib/gitlab/import_url_spec.rb
deleted file mode 100644
index f758cb8693c..00000000000
--- a/spec/lib/gitlab/import_url_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::ImportUrl do
-
- let(:credentials) { { user: 'blah', password: 'password' } }
- let(:import_url) do
- Gitlab::ImportUrl.new("https://github.com/me/project.git", credentials: credentials)
- end
-
- describe :full_url do
- it { expect(import_url.full_url).to eq("https://blah:password@github.com/me/project.git") }
- end
-
- describe :sanitized_url do
- it { expect(import_url.sanitized_url).to eq("https://github.com/me/project.git") }
- end
-
- describe :credentials do
- it { expect(import_url.credentials).to eq(credentials) }
- end
-end
diff --git a/spec/lib/gitlab/lazy_spec.rb b/spec/lib/gitlab/lazy_spec.rb
new file mode 100644
index 00000000000..b5ca89dd242
--- /dev/null
+++ b/spec/lib/gitlab/lazy_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Lazy, lib: true do
+ let(:dummy) { double(:dummy) }
+
+ context 'when not calling any methods' do
+ it 'does not call the supplied block' do
+ expect(dummy).not_to receive(:foo)
+
+ described_class.new { dummy.foo }
+ end
+ end
+
+ context 'when calling a method on the object' do
+ it 'lazy loads the value returned by the block' do
+ expect(dummy).to receive(:foo).and_return('foo')
+
+ lazy = described_class.new { dummy.foo }
+
+ expect(lazy.to_s).to eq('foo')
+ end
+ end
+
+ describe '#respond_to?' do
+ it 'returns true for a method defined on the wrapped object' do
+ lazy = described_class.new { 'foo' }
+
+ expect(lazy).to respond_to(:downcase)
+ end
+
+ it 'returns false for a method not defined on the wrapped object' do
+ lazy = described_class.new { 'foo' }
+
+ expect(lazy).not_to respond_to(:quack)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb
index 3325190789b..88814bc474d 100644
--- a/spec/lib/gitlab/lfs/lfs_router_spec.rb
+++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb
@@ -368,7 +368,7 @@ describe Gitlab::Lfs::Router, lib: true do
expect(response['objects']).to be_kind_of(Array)
expect(response['objects'].first['oid']).to eq(sample_oid)
expect(response['objects'].first['size']).to eq(sample_size)
- expect(lfs_object.projects.pluck(:id)).to_not include(project.id)
+ expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
expect(response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
expect(response['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth)
@@ -430,7 +430,7 @@ describe Gitlab::Lfs::Router, lib: true do
expect(response_body['objects'].last['oid']).to eq(sample_oid)
expect(response_body['objects'].last['size']).to eq(sample_size)
- expect(response_body['objects'].last).to_not have_key('actions')
+ expect(response_body['objects'].last).not_to have_key('actions')
end
end
end
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index 7b86450a223..cdf641341cb 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -9,9 +9,31 @@ describe Gitlab::Metrics::Instrumentation do
text
end
+ class << self
+ def buzz(text = 'buzz')
+ text
+ end
+ private :buzz
+
+ def flaky(text = 'flaky')
+ text
+ end
+ protected :flaky
+ end
+
def bar(text = 'bar')
text
end
+
+ def wadus(text = 'wadus')
+ text
+ end
+ private :wadus
+
+ def chaf(text = 'chaf')
+ text
+ end
+ protected :chaf
end
allow(@dummy).to receive(:name).and_return('Dummy')
@@ -57,7 +79,7 @@ describe Gitlab::Metrics::Instrumentation do
and_return(transaction)
expect(transaction).to receive(:add_metric).
- with(described_class::SERIES, an_instance_of(Hash),
+ with(described_class::SERIES, hash_including(:duration, :cpu_duration),
method: 'Dummy.foo')
@dummy.foo
@@ -67,7 +89,7 @@ describe Gitlab::Metrics::Instrumentation do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(100)
- expect(transaction).to_not receive(:add_metric)
+ expect(transaction).not_to receive(:add_metric)
@dummy.foo
end
@@ -137,7 +159,7 @@ describe Gitlab::Metrics::Instrumentation do
and_return(transaction)
expect(transaction).to receive(:add_metric).
- with(described_class::SERIES, an_instance_of(Hash),
+ with(described_class::SERIES, hash_including(:duration, :cpu_duration),
method: 'Dummy#bar')
@dummy.new.bar
@@ -147,7 +169,7 @@ describe Gitlab::Metrics::Instrumentation do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(100)
- expect(transaction).to_not receive(:add_metric)
+ expect(transaction).not_to receive(:add_metric)
@dummy.new.bar
end
@@ -208,6 +230,21 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_methods(@dummy)
expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
+ expect(@dummy.method(:foo).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all protected class methods' do
+ described_class.instrument_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
+ expect(@dummy.method(:flaky).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all private instance methods' do
+ described_class.instrument_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy.singleton_class)).to eq(true)
+ expect(@dummy.method(:buzz).source_location.first).to match(/instrumentation\.rb/)
end
it 'only instruments methods directly defined in the module' do
@@ -220,7 +257,7 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_methods(@dummy)
- expect(@dummy).to_not respond_to(:_original_kittens)
+ expect(@dummy).not_to respond_to(:_original_kittens)
end
it 'can take a block to determine if a method should be instrumented' do
@@ -228,7 +265,7 @@ describe Gitlab::Metrics::Instrumentation do
false
end
- expect(@dummy).to_not respond_to(:_original_foo)
+ expect(@dummy).not_to respond_to(:_original_foo)
end
end
@@ -241,6 +278,21 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_instance_methods(@dummy)
expect(described_class.instrumented?(@dummy)).to eq(true)
+ expect(@dummy.new.method(:bar).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all protected instance methods' do
+ described_class.instrument_instance_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy)).to eq(true)
+ expect(@dummy.new.method(:chaf).source_location.first).to match(/instrumentation\.rb/)
+ end
+
+ it 'instruments all private instance methods' do
+ described_class.instrument_instance_methods(@dummy)
+
+ expect(described_class.instrumented?(@dummy)).to eq(true)
+ expect(@dummy.new.method(:wadus).source_location.first).to match(/instrumentation\.rb/)
end
it 'only instruments methods directly defined in the module' do
@@ -253,7 +305,7 @@ describe Gitlab::Metrics::Instrumentation do
described_class.instrument_instance_methods(@dummy)
- expect(@dummy.method_defined?(:_original_kittens)).to eq(false)
+ expect(@dummy.new.method(:kittens).source_location.first).not_to match(/instrumentation\.rb/)
end
it 'can take a block to determine if a method should be instrumented' do
@@ -261,7 +313,7 @@ describe Gitlab::Metrics::Instrumentation do
false
end
- expect(@dummy.method_defined?(:_original_bar)).to eq(false)
+ expect(@dummy.new.method(:bar).source_location.first).not_to match(/instrumentation\.rb/)
end
end
end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index b99be4e1060..40289f8b972 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -31,6 +31,20 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.call(env)
end
+
+ it 'tags a transaction with the method andpath of the route in the grape endpoint' do
+ route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ endpoint = double(:endpoint, route: route)
+
+ env['api.endpoint'] = endpoint
+
+ allow(app).to receive(:call).with(env)
+
+ expect(middleware).to receive(:tag_endpoint).
+ with(an_instance_of(Gitlab::Metrics::Transaction), env)
+
+ middleware.call(env)
+ end
end
describe '#transaction_from_env' do
@@ -60,4 +74,19 @@ describe Gitlab::Metrics::RackMiddleware do
expect(transaction.action).to eq('TestController#show')
end
end
+
+ describe '#tag_endpoint' do
+ let(:transaction) { middleware.transaction_from_env(env) }
+
+ it 'tags a transaction with the method and path of the route in the grape endpount' do
+ route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
+ endpoint = double(:endpoint, route: route)
+
+ env['api.endpoint'] = endpoint
+
+ middleware.tag_endpoint(transaction, env)
+
+ expect(transaction.action).to eq('Grape#GET /projects/:id/archive')
+ end
+ end
end
diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb
index 38da77adc9f..1ab923b58cf 100644
--- a/spec/lib/gitlab/metrics/sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/sampler_spec.rb
@@ -72,14 +72,25 @@ describe Gitlab::Metrics::Sampler do
end
end
- describe '#sample_objects' do
- it 'adds a metric containing the amount of allocated objects' do
- expect(sampler).to receive(:add_metric).
- with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
- at_least(:once).
- and_call_original
+ if Gitlab::Metrics.mri?
+ describe '#sample_objects' do
+ it 'adds a metric containing the amount of allocated objects' do
+ expect(sampler).to receive(:add_metric).
+ with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
+ at_least(:once).
+ and_call_original
+
+ sampler.sample_objects
+ end
- sampler.sample_objects
+ it 'ignores classes without a name' do
+ expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
+
+ expect(sampler).not_to receive(:add_metric).
+ with('object_counts', an_instance_of(Hash), type: nil)
+
+ sampler.sample_objects
+ end
end
end
@@ -130,7 +141,7 @@ describe Gitlab::Metrics::Sampler do
100.times do
interval = sampler.sleep_interval
- expect(interval).to_not eq(last)
+ expect(interval).not_to eq(last)
last = interval
end
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index e3293a01207..49699ffe28f 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do
describe 'without a current transaction' do
it 'simply returns' do
expect_any_instance_of(Gitlab::Metrics::Transaction).
- to_not receive(:increment)
+ not_to receive(:increment)
subscriber.sql(event)
end
diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
new file mode 100644
index 00000000000..fd6f684db0c
--- /dev/null
+++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::Middleware::RailsQueueDuration do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+ let(:env) { {} }
+ let(:transaction) { double(:transaction) }
+
+ before { expect(app).to receive(:call).with(env).and_return('yay') }
+
+ describe '#call' do
+ it 'calls the app when metrics are disabled' do
+ expect(Gitlab::Metrics).to receive(:current_transaction).and_return(nil)
+ expect(middleware.call(env)).to eq('yay')
+ end
+
+ context 'when metrics are enabled' do
+ before { allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) }
+
+ it 'calls the app when metrics are enabled but no timing header is found' do
+ expect(middleware.call(env)).to eq('yay')
+ end
+
+ it 'sets proxy_flight_time and calls the app when the header is present' do
+ env['HTTP_GITLAB_WORHORSE_PROXY_START'] = '123'
+ expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float))
+ expect(middleware.call(env)).to eq('yay')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb
index f093d0a0d8b..e848d88182f 100644
--- a/spec/lib/gitlab/note_data_builder_spec.rb
+++ b/spec/lib/gitlab/note_data_builder_spec.rb
@@ -9,7 +9,8 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
before(:each) do
expect(data).to have_key(:object_attributes)
expect(data[:object_attributes]).to have_key(:url)
- expect(data[:object_attributes][:url]).to eq(Gitlab::UrlBuilder.build(note))
+ expect(data[:object_attributes][:url])
+ .to eq(Gitlab::UrlBuilder.build(note))
expect(data[:object_kind]).to eq('note')
expect(data[:user]).to eq(user.hook_attrs)
end
@@ -37,13 +38,21 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
end
describe 'When asking for a note on issue' do
- let(:issue) { create(:issue, created_at: fixed_time, updated_at: fixed_time) }
- let(:note) { create(:note_on_issue, noteable_id: issue.id, project: project) }
+ let(:issue) do
+ create(:issue, created_at: fixed_time, updated_at: fixed_time,
+ project: project)
+ end
+
+ let(:note) do
+ create(:note_on_issue, noteable: issue, project: project)
+ end
it 'returns the note and issue-specific data' do
expect(data).to have_key(:issue)
- expect(data[:issue].except('updated_at')).to eq(issue.hook_attrs.except('updated_at'))
- expect(data[:issue]['updated_at']).to be > issue.hook_attrs['updated_at']
+ expect(data[:issue].except('updated_at'))
+ .to eq(issue.reload.hook_attrs.except('updated_at'))
+ expect(data[:issue]['updated_at'])
+ .to be > issue.hook_attrs['updated_at']
end
include_examples 'project hook data'
@@ -51,13 +60,23 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
end
describe 'When asking for a note on merge request' do
- let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) }
- let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id, project: project) }
+ let(:merge_request) do
+ create(:merge_request, created_at: fixed_time,
+ updated_at: fixed_time,
+ source_project: project)
+ end
+
+ let(:note) do
+ create(:note_on_merge_request, noteable: merge_request,
+ project: project)
+ end
it 'returns the note and merge request data' do
expect(data).to have_key(:merge_request)
- expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at'))
- expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at']
+ expect(data[:merge_request].except('updated_at'))
+ .to eq(merge_request.reload.hook_attrs.except('updated_at'))
+ expect(data[:merge_request]['updated_at'])
+ .to be > merge_request.hook_attrs['updated_at']
end
include_examples 'project hook data'
@@ -65,13 +84,22 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
end
describe 'When asking for a note on merge request diff' do
- let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) }
- let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id, project: project) }
+ let(:merge_request) do
+ create(:merge_request, created_at: fixed_time, updated_at: fixed_time,
+ source_project: project)
+ end
+
+ let(:note) do
+ create(:note_on_merge_request_diff, noteable: merge_request,
+ project: project)
+ end
it 'returns the note and merge request diff data' do
expect(data).to have_key(:merge_request)
- expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at'))
- expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at']
+ expect(data[:merge_request].except('updated_at'))
+ .to eq(merge_request.reload.hook_attrs.except('updated_at'))
+ expect(data[:merge_request]['updated_at'])
+ .to be > merge_request.hook_attrs['updated_at']
end
include_examples 'project hook data'
@@ -79,13 +107,22 @@ describe 'Gitlab::NoteDataBuilder', lib: true do
end
describe 'When asking for a note on project snippet' do
- let!(:snippet) { create(:project_snippet, created_at: fixed_time, updated_at: fixed_time) }
- let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id, project: project) }
+ let!(:snippet) do
+ create(:project_snippet, created_at: fixed_time, updated_at: fixed_time,
+ project: project)
+ end
+
+ let!(:note) do
+ create(:note_on_project_snippet, noteable: snippet,
+ project: project)
+ end
it 'returns the note and project snippet data' do
expect(data).to have_key(:snippet)
- expect(data[:snippet].except('updated_at')).to eq(snippet.hook_attrs.except('updated_at'))
- expect(data[:snippet]['updated_at']).to be > snippet.hook_attrs['updated_at']
+ expect(data[:snippet].except('updated_at'))
+ .to eq(snippet.reload.hook_attrs.except('updated_at'))
+ expect(data[:snippet]['updated_at'])
+ .to be > snippet.hook_attrs['updated_at']
end
include_examples 'project hook data'
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index db0ff95b4f5..270b89972d7 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -43,6 +43,18 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 1
end
+ it 'should not list project confidential issues for project members with guest role' do
+ project.team << [member, :guest]
+
+ results = described_class.new(member, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(results.issues_count).to eq 1
+ end
+
it 'should list project confidential issues for author' do
results = described_class.new(author, project, query)
issues = results.objects('issues')
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index c2a51d9249c..84c21ceefd9 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -145,6 +145,7 @@ describe Gitlab::Saml::User, lib: true do
allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+ allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
end
context 'and no account for the LDAP user' do
@@ -177,6 +178,23 @@ describe Gitlab::Saml::User, lib: true do
])
end
end
+
+ context 'user has SAML user, and wants to add their LDAP identity' do
+ it 'adds the LDAP identity to the existing SAML user' do
+ create(:omniauth_user, email: 'john@mail.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'saml', username: 'john')
+ local_hash = OmniAuth::AuthHash.new(uid: 'uid=user1,ou=People,dc=example', provider: provider, info: info_hash)
+ local_saml_user = described_class.new(local_hash)
+ local_saml_user.save
+ local_gl_user = local_saml_user.gl_user
+
+ expect(local_gl_user).to be_valid
+ expect(local_gl_user.identities.length).to eql 2
+ identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'saml', extern_uid: 'uid=user1,ou=People,dc=example' }
+ ])
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/sanitizers/svg_spec.rb b/spec/lib/gitlab/sanitizers/svg_spec.rb
new file mode 100644
index 00000000000..030c2063ab2
--- /dev/null
+++ b/spec/lib/gitlab/sanitizers/svg_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::Sanitizers::SVG do
+ let(:scrubber) { Gitlab::Sanitizers::SVG::Scrubber.new }
+ let(:namespace) { double(Nokogiri::XML::Namespace, prefix: 'xlink', href: 'http://www.w3.org/1999/xlink') }
+ let(:namespaced_attr) { double(Nokogiri::XML::Attr, name: 'href', namespace: namespace, value: '#awesome_id') }
+
+ describe '.clean' do
+ let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
+ let(:data) { open(input_svg_path).read }
+ let(:sanitized_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
+ let(:sanitized) { open(sanitized_svg_path).read }
+
+ it 'delegates sanitization to scrubber' do
+ expect_any_instance_of(Gitlab::Sanitizers::SVG::Scrubber).to receive(:scrub).at_least(:once)
+ described_class.clean(data)
+ end
+
+ it 'returns sanitized data' do
+ expect(described_class.clean(data)).to eq(sanitized)
+ end
+ end
+
+ context 'scrubber' do
+ describe '#scrub' do
+ let(:invalid_element) { double(Nokogiri::XML::Node, name: 'invalid', value: 'invalid') }
+ let(:invalid_attribute) { double(Nokogiri::XML::Attr, name: 'invalid', namespace: nil) }
+ let(:valid_element) { double(Nokogiri::XML::Node, name: 'use') }
+
+ it 'removes an invalid element' do
+ expect(invalid_element).to receive(:unlink)
+
+ scrubber.scrub(invalid_element)
+ end
+
+ it 'removes an invalid attribute' do
+ allow(valid_element).to receive(:attribute_nodes) { [invalid_attribute] }
+ expect(invalid_attribute).to receive(:unlink)
+
+ scrubber.scrub(valid_element)
+ end
+
+ it 'accepts valid element' do
+ allow(valid_element).to receive(:attribute_nodes) { [namespaced_attr] }
+ expect(valid_element).not_to receive(:unlink)
+
+ scrubber.scrub(valid_element)
+ end
+
+ it 'accepts valid namespaced attributes' do
+ allow(valid_element).to receive(:attribute_nodes) { [namespaced_attr] }
+ expect(namespaced_attr).not_to receive(:unlink)
+
+ scrubber.scrub(valid_element)
+ end
+ end
+
+ describe '#attribute_name_with_namespace' do
+ it 'returns name with prefix when attribute is namespaced' do
+ expect(scrubber.attribute_name_with_namespace(namespaced_attr)).to eq('xlink:href')
+ end
+ end
+
+ describe '#unsafe_href?' do
+ let(:unsafe_attr) { double(Nokogiri::XML::Attr, name: 'href', namespace: namespace, value: 'http://evilsite.example.com/random.svg') }
+
+ it 'returns true if href attribute is an external url' do
+ expect(scrubber.unsafe_href?(unsafe_attr)).to be_truthy
+ end
+
+ it 'returns false if href atttribute is an internal reference' do
+ expect(scrubber.unsafe_href?(namespaced_attr)).to be_falsey
+ end
+ end
+
+ describe '#data_attribute?' do
+ let(:data_attr) { double(Nokogiri::XML::Attr, name: 'data-gitlab', namespace: nil, value: 'gitlab is awesome') }
+ let(:namespaced_attr) { double(Nokogiri::XML::Attr, name: 'data-gitlab', namespace: namespace, value: 'gitlab is awesome') }
+ let(:other_attr) { double(Nokogiri::XML::Attr, name: 'something', namespace: nil, value: 'content') }
+
+ it 'returns true if is a valid data attribute' do
+ expect(scrubber.data_attribute?(data_attr)).to be_truthy
+ end
+
+ it 'returns false if attribute is namespaced' do
+ expect(scrubber.data_attribute?(namespaced_attr)).to be_falsey
+ end
+
+ it 'returns false if not a data attribute' do
+ expect(scrubber.data_attribute?(other_attr)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index f4afe597e8d..1bb444bf34f 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -86,6 +86,22 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 1
end
+ it 'should not list confidential issues for project members with guest role' do
+ project_1.team << [member, :guest]
+ project_2.team << [member, :guest]
+
+ results = described_class.new(member, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(issues).not_to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 1
+ end
+
it 'should list confidential issues for author' do
results = described_class.new(author, limit_projects, query)
issues = results.objects('issues')
diff --git a/spec/lib/gitlab/sherlock/collection_spec.rb b/spec/lib/gitlab/sherlock/collection_spec.rb
index de6bb86c5dd..2ae79b50e77 100644
--- a/spec/lib/gitlab/sherlock/collection_spec.rb
+++ b/spec/lib/gitlab/sherlock/collection_spec.rb
@@ -11,13 +11,13 @@ describe Gitlab::Sherlock::Collection, lib: true do
it 'adds a new transaction' do
collection.add(transaction)
- expect(collection).to_not be_empty
+ expect(collection).not_to be_empty
end
it 'is aliased as <<' do
collection << transaction
- expect(collection).to_not be_empty
+ expect(collection).not_to be_empty
end
end
@@ -47,7 +47,7 @@ describe Gitlab::Sherlock::Collection, lib: true do
it 'returns false for a collection with a transaction' do
collection.add(transaction)
- expect(collection).to_not be_empty
+ expect(collection).not_to be_empty
end
end
diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb
index 05da915ccfd..0a620428138 100644
--- a/spec/lib/gitlab/sherlock/query_spec.rb
+++ b/spec/lib/gitlab/sherlock/query_spec.rb
@@ -85,7 +85,7 @@ FROM users;
frames = query.application_backtrace
expect(frames).to be_an_instance_of(Array)
- expect(frames).to_not be_empty
+ expect(frames).not_to be_empty
frames.each do |frame|
expect(frame.path).to start_with(Rails.root.to_s)
diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb
index 7553f2a045f..9fe18f253f0 100644
--- a/spec/lib/gitlab/sherlock/transaction_spec.rb
+++ b/spec/lib/gitlab/sherlock/transaction_spec.rb
@@ -203,7 +203,7 @@ describe Gitlab::Sherlock::Transaction, lib: true do
end
it 'only tracks queries triggered from the transaction thread' do
- expect(transaction).to_not receive(:track_query)
+ expect(transaction).not_to receive(:track_query)
Thread.new { subscription.publish('test', time, time, nil, query_data) }.
join
@@ -226,7 +226,7 @@ describe Gitlab::Sherlock::Transaction, lib: true do
end
it 'only tracks views rendered from the transaction thread' do
- expect(transaction).to_not receive(:track_view)
+ expect(transaction).not_to receive(:track_view)
Thread.new { subscription.publish('test', time, time, nil, view_data) }.
join
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
new file mode 100644
index 00000000000..de55334118f
--- /dev/null
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe Gitlab::UrlSanitizer, lib: true do
+ let(:credentials) { { user: 'blah', password: 'password' } }
+ let(:url_sanitizer) do
+ described_class.new("https://github.com/me/project.git", credentials: credentials)
+ end
+
+ describe '.sanitize' do
+ def sanitize_url(url)
+ # We want to try with multi-line content because is how error messages are formatted
+ described_class.sanitize(%Q{
+ remote: Not Found
+ fatal: repository '#{url}' not found
+ })
+ end
+
+ it 'mask the credentials from HTTP URLs' do
+ filtered_content = sanitize_url('http://user:pass@test.com/root/repoC.git/')
+
+ expect(filtered_content).to include("http://*****:*****@test.com/root/repoC.git/")
+ end
+
+ it 'mask the credentials from HTTPS URLs' do
+ filtered_content = sanitize_url('https://user:pass@test.com/root/repoA.git/')
+
+ expect(filtered_content).to include("https://*****:*****@test.com/root/repoA.git/")
+ end
+
+ it 'mask credentials from SSH URLs' do
+ filtered_content = sanitize_url('ssh://user@host.test/path/to/repo.git')
+
+ expect(filtered_content).to include("ssh://*****@host.test/path/to/repo.git")
+ end
+
+ it 'does not modify Git URLs' do
+ # git protocol does not support authentication
+ filtered_content = sanitize_url('git://host.test/path/to/repo.git')
+
+ expect(filtered_content).to include("git://host.test/path/to/repo.git")
+ end
+
+ it 'does not modify scp-like URLs' do
+ filtered_content = sanitize_url('user@server:project.git')
+
+ expect(filtered_content).to include("user@server:project.git")
+ end
+ end
+
+ describe '#sanitized_url' do
+ it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") }
+ end
+
+ describe '#credentials' do
+ it { expect(url_sanitizer.credentials).to eq(credentials) }
+ end
+
+ describe '#full_url' do
+ it { expect(url_sanitizer.full_url).to eq("https://blah:password@github.com/me/project.git") }
+
+ it 'supports scp-like URLs' do
+ sanitizer = described_class.new('user@server:project.git')
+
+ expect(sanitizer.full_url).to eq('user@server:project.git')
+ end
+ end
+
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index d940bf05061..c5c1402e8fc 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Workhorse, lib: true do
end
it "raises an error" do
- expect { subject.send_git_archive(project, "master", "zip") }.to raise_error(RuntimeError)
+ expect { subject.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
end
end
end
diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb
index 0c3d3ea7019..18726754517 100644
--- a/spec/lib/json_web_token/rsa_token_spec.rb
+++ b/spec/lib/json_web_token/rsa_token_spec.rb
@@ -23,7 +23,7 @@ describe JSONWebToken::RSAToken do
subject { JWT.decode(rsa_encoded, rsa_key) }
- it { expect{subject}.to_not raise_error }
+ it { expect{subject}.not_to raise_error }
it { expect(subject.first).to include('key' => 'value') }
it do
expect(subject.second).to eq(
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 5f7e4a526e6..1e6eb20ab39 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -51,7 +51,7 @@ describe Notify do
context 'when enabled email_author_in_body' do
before do
- allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true)
end
it 'contains a link to note author' do
@@ -230,7 +230,7 @@ describe Notify do
context 'when enabled email_author_in_body' do
before do
- allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true)
end
it 'contains a link to note author' do
@@ -400,26 +400,136 @@ describe Notify do
end
end
+ describe 'project access requested' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:project_member) do
+ project.request_access(user)
+ project.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_requested_email('project', project_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
+ is_expected.to have_body_text /#{project_member.human_access}/
+ end
+ end
+
+ describe 'project access denied' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:project_member) do
+ project.request_access(user)
+ project.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_denied_email('project', project.id, user.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ end
+ end
+
describe 'project access changed' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
- subject { Notify.project_access_granted_email(project_member.id) }
+ subject { Notify.member_access_granted_email('project', project_member.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
- it 'has the correct subject' do
- is_expected.to have_subject /Access to project was granted/
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ is_expected.to have_body_text /#{project_member.human_access}/
end
+ end
- it 'contains name of project' do
- is_expected.to have_body_text /#{project.name}/
- end
+ def invite_to_project(project:, email:, inviter:)
+ ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
- it 'contains new user role' do
+ project.project_members.invite.last
+ end
+
+ describe 'project invitation' do
+ let(:project) { create(:project) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) }
+
+ subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
is_expected.to have_body_text /#{project_member.human_access}/
+ is_expected.to have_body_text /#{project_member.invite_token}/
+ end
+ end
+
+ describe 'project invitation accepted' do
+ let(:project) { create(:project) }
+ let(:invited_user) { create(:user) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:project_member) do
+ invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+ invitee.accept_invite!(invited_user)
+ invitee
+ end
+
+ subject { Notify.member_invite_accepted_email('project', project_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation accepted'
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ is_expected.to have_body_text /#{project_member.invite_email}/
+ is_expected.to have_body_text /#{invited_user.name}/
+ end
+ end
+
+ describe 'project invitation declined' do
+ let(:project) { create(:project) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:project_member) do
+ invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+ invitee.decline_invite!
+ invitee
+ end
+
+ subject { Notify.member_invite_declined_email('project', project.id, project_member.invite_email, master.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation declined'
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{project.web_url}/
+ is_expected.to have_body_text /#{project_member.invite_email}/
end
end
@@ -454,7 +564,7 @@ describe Notify do
context 'when enabled email_author_in_body' do
before do
- allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:email_author_in_body).and_return(true)
end
it 'contains a link to note author' do
@@ -535,27 +645,139 @@ describe Notify do
end
end
- describe 'group access changed' do
- let(:group) { create(:group) }
- let(:user) { create(:user) }
- let(:membership) { create(:group_member, group: group, user: user) }
+ context 'for a group' do
+ describe 'group access requested' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:group_member) do
+ group.request_access(user)
+ group.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_requested_email('group', group_member.id) }
- subject { Notify.group_access_granted_email(membership.id) }
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Request to join the #{group.name} group"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group_group_members_url(group)}/
+ is_expected.to have_body_text /#{group_member.human_access}/
+ end
+ end
- it 'has the correct subject' do
- is_expected.to have_subject /Access to group was granted/
+ describe 'group access denied' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:group_member) do
+ group.request_access(user)
+ group.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_denied_email('group', group.id, user.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{group.name} group was denied"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ end
+ end
+
+ describe 'group access changed' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+ let(:group_member) { create(:group_member, group: group, user: user) }
+
+ subject { Notify.member_access_granted_email('group', group_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Access to the #{group.name} group was granted"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.human_access}/
+ end
+ end
+
+ def invite_to_group(group:, email:, inviter:)
+ GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+
+ group.group_members.invite.last
end
- it 'contains name of project' do
- is_expected.to have_body_text /#{group.name}/
+ describe 'group invitation' do
+ let(:group) { create(:group) }
+ let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+ let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) }
+
+ subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject "Invitation to join the #{group.name} group"
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.human_access}/
+ is_expected.to have_body_text /#{group_member.invite_token}/
+ end
end
- it 'contains new user role' do
- is_expected.to have_body_text /#{membership.human_access}/
+ describe 'group invitation accepted' do
+ let(:group) { create(:group) }
+ let(:invited_user) { create(:user) }
+ let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+ let(:group_member) do
+ invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+ invitee.accept_invite!(invited_user)
+ invitee
+ end
+
+ subject { Notify.member_invite_accepted_email('group', group_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation accepted'
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.invite_email}/
+ is_expected.to have_body_text /#{invited_user.name}/
+ end
+ end
+
+ describe 'group invitation declined' do
+ let(:group) { create(:group) }
+ let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
+ let(:group_member) do
+ invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+ invitee.decline_invite!
+ invitee
+ end
+
+ subject { Notify.member_invite_declined_email('group', group.id, group_member.invite_email, owner.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ is_expected.to have_subject 'Invitation declined'
+ is_expected.to have_body_text /#{group.name}/
+ is_expected.to have_body_text /#{group.web_url}/
+ is_expected.to have_body_text /#{group_member.invite_email}/
+ end
end
end
@@ -693,8 +915,9 @@ describe Notify do
let(:commits) { Commit.decorate(compare.commits, nil) }
let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) }
let(:send_from_committer_email) { false }
+ let(:diff_refs) { [project.merge_base_commit(sample_image_commit.id, sample_commit.id), project.commit(sample_commit.id)] }
- subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) }
+ subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, diff_refs: diff_refs, send_from_committer_email: send_from_committer_email) }
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
@@ -715,15 +938,15 @@ describe Notify do
is_expected.to have_body_text /Change some files/
end
- it 'includes diffs' do
- is_expected.to have_body_text /def archive_formats_regex/
+ it 'includes diffs with character-level highlighting' do
+ is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
end
it 'contains a link to the diff' do
is_expected.to have_body_text /#{diff_path}/
end
- it 'doesn not contain the misleading footer' do
+ it 'does not contain the misleading footer' do
is_expected.not_to have_body_text /you are a member of/
end
@@ -797,8 +1020,9 @@ describe Notify do
let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
let(:commits) { Commit.decorate(compare.commits, nil) }
let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) }
+ let(:diff_refs) { [project.merge_base_commit(sample_commit.parent_id, sample_commit.id), project.commit(sample_commit.id)] }
- subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
+ subject { Notify.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, diff_refs: diff_refs) }
it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like "a user cannot unsubscribe through footer link"
@@ -819,8 +1043,8 @@ describe Notify do
is_expected.to have_body_text /Change some files/
end
- it 'includes diffs' do
- is_expected.to have_body_text /def archive_formats_regex/
+ it 'includes diffs with character-level highlighting' do
+ is_expected.to have_body_text /def<\/span> <span class=\"nf\">archive_formats_regex/
end
it 'contains a link to the diff' do
diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/spec/mailers/previews/devise_mailer_preview.rb
new file mode 100644
index 00000000000..dc3062a4332
--- /dev/null
+++ b/spec/mailers/previews/devise_mailer_preview.rb
@@ -0,0 +1,11 @@
+class DeviseMailerPreview < ActionMailer::Preview
+ def confirmation_instructions_for_signup
+ user = User.new(name: 'Jane Doe', email: 'signup@example.com')
+ DeviseMailer.confirmation_instructions(user, 'faketoken', {})
+ end
+
+ def confirmation_instructions_for_new_email
+ user = User.last
+ DeviseMailer.confirmation_instructions(user, 'faketoken', {})
+ end
+end
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
index 5a85cb501dd..93de5850ba2 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/mailers/shared/notify.rb
@@ -146,8 +146,8 @@ shared_examples 'it should have Gmail Actions links' do
end
shared_examples 'it should not have Gmail Actions links' do
- it { is_expected.to_not have_body_text '<script type="application/ld+json">' }
- it { is_expected.to_not have_body_text /ViewAction/ }
+ it { is_expected.not_to have_body_text '<script type="application/ld+json">' }
+ it { is_expected.not_to have_body_text /ViewAction/ }
end
shared_examples 'it should show Gmail Actions View Issue link' do
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
new file mode 100644
index 00000000000..1acb5846fcf
--- /dev/null
+++ b/spec/models/ability_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Ability, lib: true do
+ describe '.users_that_can_read_project' do
+ context 'using a public project' do
+ it 'returns all the users' do
+ project = create(:project, :public)
+ user = build(:user)
+
+ expect(described_class.users_that_can_read_project([user], project)).
+ to eq([user])
+ end
+ end
+
+ context 'using an internal project' do
+ let(:project) { create(:project, :internal) }
+
+ it 'returns users that are administrators' do
+ user = build(:user, admin: true)
+
+ expect(described_class.users_that_can_read_project([user], project)).
+ to eq([user])
+ end
+
+ it 'returns internal users while skipping external users' do
+ user1 = build(:user)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([user1])
+ end
+
+ it 'returns external users if they are the project owner' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(project).to receive(:owner).twice.and_return(user1)
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([user1])
+ end
+
+ it 'returns external users if they are project members' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(project.team).to receive(:members).twice.and_return([user1])
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([user1])
+ end
+
+ it 'returns an empty Array if all users are external users without access' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([])
+ end
+ end
+
+ context 'using a private project' do
+ let(:project) { create(:project, :private) }
+
+ it 'returns users that are administrators' do
+ user = build(:user, admin: true)
+
+ expect(described_class.users_that_can_read_project([user], project)).
+ to eq([user])
+ end
+
+ it 'returns external users if they are the project owner' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(project).to receive(:owner).twice.and_return(user1)
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([user1])
+ end
+
+ it 'returns external users if they are project members' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(project.team).to receive(:members).twice.and_return([user1])
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([user1])
+ end
+
+ it 'returns an empty Array if all users are internal users without access' do
+ user1 = build(:user)
+ user2 = build(:user)
+ users = [user1, user2]
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([])
+ end
+
+ it 'returns an empty Array if all users are external users without access' do
+ user1 = build(:user, external: true)
+ user2 = build(:user, external: true)
+ users = [user1, user2]
+
+ expect(described_class.users_that_can_read_project(users, project)).
+ to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
new file mode 100644
index 00000000000..cb3c592f8cd
--- /dev/null
+++ b/spec/models/award_emoji_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe AwardEmoji, models: true do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:awardable) }
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'modules' do
+ it { is_expected.to include_module(Participable) }
+ end
+
+ describe "validations" do
+ it { is_expected.to validate_presence_of(:awardable) }
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:name) }
+
+ # To circumvent a bug in the shoulda matchers
+ describe "scoped uniqueness validation" do
+ it "rejects duplicate award emoji" do
+ user = create(:user)
+ issue = create(:issue)
+ create(:award_emoji, user: user, awardable: issue)
+ new_award = build(:award_emoji, user: user, awardable: issue)
+
+ expect(new_award).not_to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index b5d356aa066..5d1fa8226e5 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -1,18 +1,17 @@
require 'spec_helper'
describe Ci::Build, models: true do
- let(:project) { FactoryGirl.create :project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
- let(:build) { FactoryGirl.create :ci_build, commit: commit }
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to validate_presence_of :ref }
it { is_expected.to respond_to :trace_html }
describe '#first_pending' do
- let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday }
- let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' }
- before { first; second }
+ let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) }
+ let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') }
subject { Ci::Build.first_pending }
it { is_expected.to be_a(Ci::Build) }
@@ -90,7 +89,7 @@ describe Ci::Build, models: true do
build.update_attributes(trace: token)
end
- it { is_expected.to_not include(token) }
+ it { is_expected.not_to include(token) }
end
end
@@ -98,7 +97,7 @@ describe Ci::Build, models: true do
# describe :timeout do
# subject { build.timeout }
#
- # it { is_expected.to eq(commit.project.timeout) }
+ # it { is_expected.to eq(pipeline.project.timeout) }
# end
describe '#options' do
@@ -125,13 +124,13 @@ describe Ci::Build, models: true do
describe '#project' do
subject { build.project }
- it { is_expected.to eq(commit.project) }
+ it { is_expected.to eq(pipeline.project) }
end
describe '#project_id' do
subject { build.project_id }
- it { is_expected.to eq(commit.project_id) }
+ it { is_expected.to eq(pipeline.project_id) }
end
describe '#project_name' do
@@ -219,8 +218,8 @@ describe Ci::Build, models: true do
it { is_expected.to eq(predefined_variables + yaml_variables + secure_variables) }
context 'and trigger variables' do
- let(:trigger) { FactoryGirl.create :ci_trigger, project: project }
- let(:trigger_request) { FactoryGirl.create :ci_trigger_request_with_variables, commit: commit, trigger: trigger }
+ let(:trigger) { create(:ci_trigger, project: project) }
+ let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
let(:trigger_variables) do
[
{ key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false }
@@ -259,11 +258,11 @@ describe Ci::Build, models: true do
end
describe '#can_be_served?' do
- let(:runner) { FactoryGirl.create :ci_runner }
+ let(:runner) { create(:ci_runner) }
before { build.project.runners << runner }
- context 'runner without tags' do
+ context 'when runner does not have tags' do
it 'can handle builds without tags' do
expect(build.can_be_served?(runner)).to be_truthy
end
@@ -274,25 +273,53 @@ describe Ci::Build, models: true do
end
end
- context 'runner with tags' do
+ context 'when runner has tags' do
before { runner.tag_list = ['bb', 'cc'] }
- it 'can handle builds without tags' do
- expect(build.can_be_served?(runner)).to be_truthy
+ shared_examples 'tagged build picker' do
+ it 'can handle build with matching tags' do
+ build.tag_list = ['bb']
+ expect(build.can_be_served?(runner)).to be_truthy
+ end
+
+ it 'cannot handle build without matching tags' do
+ build.tag_list = ['aa']
+ expect(build.can_be_served?(runner)).to be_falsey
+ end
end
- it 'can handle build with matching tags' do
- build.tag_list = ['bb']
- expect(build.can_be_served?(runner)).to be_truthy
+ context 'when runner can pick untagged jobs' do
+ it 'can handle builds without tags' do
+ expect(build.can_be_served?(runner)).to be_truthy
+ end
+
+ it_behaves_like 'tagged build picker'
end
- it 'cannot handle build with not matching tags' do
- build.tag_list = ['aa']
- expect(build.can_be_served?(runner)).to be_falsey
+ context 'when runner can not pick untagged jobs' do
+ before { runner.run_untagged = false }
+
+ it 'can not handle builds without tags' do
+ expect(build.can_be_served?(runner)).to be_falsey
+ end
+
+ it_behaves_like 'tagged build picker'
end
end
end
+ describe '#has_tags?' do
+ context 'when build has tags' do
+ subject { create(:ci_build, tag_list: ['tag']) }
+ it { is_expected.to have_tags }
+ end
+
+ context 'when build does not have tags' do
+ subject { create(:ci_build, tag_list: []) }
+ it { is_expected.not_to have_tags }
+ end
+ end
+
describe '#any_runners_online?' do
subject { build.any_runners_online? }
@@ -301,7 +328,7 @@ describe Ci::Build, models: true do
end
context 'if there are runner' do
- let(:runner) { FactoryGirl.create :ci_runner }
+ let(:runner) { create(:ci_runner) }
before do
build.project.runners << runner
@@ -338,7 +365,7 @@ describe Ci::Build, models: true do
it { is_expected.to be_truthy }
context "and there are specific runner" do
- let(:runner) { FactoryGirl.create :ci_runner, contacted_at: 1.second.ago }
+ let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
before do
build.project.runners << runner
@@ -370,9 +397,34 @@ describe Ci::Build, models: true do
context 'artifacts archive exists' do
let(:build) { create(:ci_build, :artifacts) }
it { is_expected.to be_truthy }
+
+ context 'is expired' do
+ before { build.update(artifacts_expire_at: Time.now - 7.days) }
+ it { is_expected.to be_falsy }
+ end
+
+ context 'is not expired' do
+ before { build.update(artifacts_expire_at: Time.now + 7.days) }
+ it { is_expected.to be_truthy }
+ end
end
end
+ describe '#artifacts_expired?' do
+ subject { build.artifacts_expired? }
+
+ context 'is expired' do
+ before { build.update(artifacts_expire_at: Time.now - 7.days) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'is not expired' do
+ before { build.update(artifacts_expire_at: Time.now + 7.days) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
describe '#artifacts_metadata?' do
subject { build.artifacts_metadata? }
@@ -385,9 +437,8 @@ describe Ci::Build, models: true do
it { is_expected.to be_truthy }
end
end
-
describe '#repo_url' do
- let(:build) { FactoryGirl.create :ci_build }
+ let(:build) { create(:ci_build) }
let(:project) { build.project }
subject { build.repo_url }
@@ -400,11 +451,55 @@ describe Ci::Build, models: true do
it { is_expected.to include(project.web_url[7..-1]) }
end
+ describe '#artifacts_expire_in' do
+ subject { build.artifacts_expire_in }
+ it { is_expected.to be_nil }
+
+ context 'when artifacts_expire_at is specified' do
+ let(:expire_at) { Time.now + 7.days }
+
+ before { build.artifacts_expire_at = expire_at }
+
+ it { is_expected.to be_within(5).of(expire_at - Time.now) }
+ end
+ end
+
+ describe '#artifacts_expire_in=' do
+ subject { build.artifacts_expire_in }
+
+ it 'when assigning valid duration' do
+ build.artifacts_expire_in = '7 days'
+
+ is_expected.to be_within(10).of(7.days.to_i)
+ end
+
+ it 'when assigning invalid duration' do
+ expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError)
+ is_expected.to be_nil
+ end
+
+ it 'when resseting value' do
+ build.artifacts_expire_in = nil
+
+ is_expected.to be_nil
+ end
+ end
+
+ describe '#keep_artifacts!' do
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) }
+
+ it 'to reset expire_at' do
+ build.keep_artifacts!
+
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ end
+
describe '#depends_on_builds' do
- let!(:build) { FactoryGirl.create :ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build' }
- let!(:rspec_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test' }
- let!(:rubocop_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test' }
- let!(:staging) { FactoryGirl.create :ci_build, commit: commit, name: 'staging', stage_idx: 2, stage: 'deploy' }
+ let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') }
+ let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') }
+ let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
+ let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
it 'to have no dependents if this is first build' do
expect(build.depends_on_builds).to be_empty
@@ -424,20 +519,19 @@ describe Ci::Build, models: true do
end
end
- def create_mr(build, commit, factory: :merge_request, created_at: Time.now)
- FactoryGirl.create(factory,
- source_project_id: commit.gl_project_id,
- target_project_id: commit.gl_project_id,
- source_branch: build.ref,
- created_at: created_at)
+ def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
+ create(factory, source_project_id: pipeline.gl_project_id,
+ target_project_id: pipeline.gl_project_id,
+ source_branch: build.ref,
+ created_at: created_at)
end
describe '#merge_request' do
- context 'when a MR has a reference to the commit' do
+ context 'when a MR has a reference to the pipeline' do
before do
- @merge_request = create_mr(build, commit, factory: :merge_request)
+ @merge_request = create_mr(build, pipeline, factory: :merge_request)
- commits = [double(id: commit.sha)]
+ commits = [double(id: pipeline.sha)]
allow(@merge_request).to receive(:commits).and_return(commits)
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
end
@@ -447,19 +541,19 @@ describe Ci::Build, models: true do
end
end
- context 'when there is not a MR referencing the commit' do
+ context 'when there is not a MR referencing the pipeline' do
it 'returns nil' do
expect(build.merge_request).to be_nil
end
end
- context 'when more than one MR have a reference to the commit' do
+ context 'when more than one MR have a reference to the pipeline' do
before do
- @merge_request = create_mr(build, commit, factory: :merge_request)
+ @merge_request = create_mr(build, pipeline, factory: :merge_request)
@merge_request.close!
- @merge_request2 = create_mr(build, commit, factory: :merge_request)
+ @merge_request2 = create_mr(build, pipeline, factory: :merge_request)
- commits = [double(id: commit.sha)]
+ commits = [double(id: pipeline.sha)]
allow(@merge_request).to receive(:commits).and_return(commits)
allow(@merge_request2).to receive(:commits).and_return(commits)
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2])
@@ -472,11 +566,11 @@ describe Ci::Build, models: true do
context 'when a Build is created after the MR' do
before do
- @merge_request = create_mr(build, commit, factory: :merge_request_with_diffs)
- commit2 = FactoryGirl.create :ci_commit, project: project
- @build2 = FactoryGirl.create :ci_build, commit: commit2
+ @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs)
+ pipeline2 = create(:ci_pipeline, project: project)
+ @build2 = create(:ci_build, pipeline: pipeline2)
- commits = [double(id: commit.sha), double(id: commit2.sha)]
+ commits = [double(id: pipeline.sha), double(id: pipeline2.sha)]
allow(@merge_request).to receive(:commits).and_return(commits)
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
end
@@ -506,7 +600,7 @@ describe Ci::Build, models: true do
end
it 'should set erase date' do
- expect(build.erased_at).to_not be_falsy
+ expect(build.erased_at).not_to be_falsy
end
end
@@ -578,7 +672,7 @@ describe Ci::Build, models: true do
describe '#erase' do
it 'should not raise error' do
- expect { build.erase }.to_not raise_error
+ expect { build.erase }.not_to raise_error
end
end
end
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
deleted file mode 100644
index dc071ad1c90..00000000000
--- a/spec/models/ci/commit_spec.rb
+++ /dev/null
@@ -1,404 +0,0 @@
-require 'spec_helper'
-
-describe Ci::Commit, models: true do
- let(:project) { FactoryGirl.create :empty_project }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
-
- it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:statuses) }
- it { is_expected.to have_many(:trigger_requests) }
- it { is_expected.to have_many(:builds) }
- it { is_expected.to validate_presence_of :sha }
- it { is_expected.to validate_presence_of :status }
- it { is_expected.to delegate_method(:stages).to(:statuses) }
-
- it { is_expected.to respond_to :git_author_name }
- it { is_expected.to respond_to :git_author_email }
- it { is_expected.to respond_to :short_sha }
-
- describe :valid_commit_sha do
- context 'commit.sha can not start with 00000000' do
- before do
- commit.sha = '0' * 40
- commit.valid_commit_sha
- end
-
- it('commit errors should not be empty') { expect(commit.errors).not_to be_empty }
- end
- end
-
- describe :short_sha do
- subject { commit.short_sha }
-
- it 'has 8 items' do
- expect(subject.size).to eq(8)
- end
- it { expect(commit.sha).to start_with(subject) }
- end
-
- describe :create_next_builds do
- end
-
- describe :retried do
- subject { commit.retried }
-
- before do
- @commit1 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy'
- @commit2 = FactoryGirl.create :ci_build, commit: commit, name: 'deploy'
- end
-
- it 'returns old builds' do
- is_expected.to contain_exactly(@commit1)
- end
- end
-
- describe :create_builds do
- let!(:commit) { FactoryGirl.create :ci_commit, project: project, ref: 'master', tag: false }
-
- def create_builds(trigger_request = nil)
- commit.create_builds(nil, trigger_request)
- end
-
- def create_next_builds
- commit.create_next_builds(commit.builds.order(:id).last)
- end
-
- it 'creates builds' do
- expect(create_builds).to be_truthy
- commit.builds.update_all(status: "success")
- expect(commit.builds.count(:all)).to eq(2)
-
- expect(create_next_builds).to be_truthy
- commit.builds.update_all(status: "success")
- expect(commit.builds.count(:all)).to eq(4)
-
- expect(create_next_builds).to be_truthy
- commit.builds.update_all(status: "success")
- expect(commit.builds.count(:all)).to eq(5)
-
- expect(create_next_builds).to be_falsey
- end
-
- context 'custom stage with first job allowed to fail' do
- let(:yaml) do
- {
- stages: ['clean', 'test'],
- clean_job: {
- stage: 'clean',
- allow_failure: true,
- script: 'BUILD',
- },
- test_job: {
- stage: 'test',
- script: 'TEST',
- },
- }
- end
-
- before do
- stub_ci_commit_yaml_file(YAML.dump(yaml))
- create_builds
- end
-
- it 'properly schedules builds' do
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:drop)
- expect(commit.builds.pluck(:status)).to contain_exactly('pending', 'failed')
- end
- end
-
- context 'properly creates builds when "when" is defined' do
- let(:yaml) do
- {
- stages: ["build", "test", "test_failure", "deploy", "cleanup"],
- build: {
- stage: "build",
- script: "BUILD",
- },
- test: {
- stage: "test",
- script: "TEST",
- },
- test_failure: {
- stage: "test_failure",
- script: "ON test failure",
- when: "on_failure",
- },
- deploy: {
- stage: "deploy",
- script: "PUBLISH",
- },
- cleanup: {
- stage: "cleanup",
- script: "TIDY UP",
- when: "always",
- }
- }
- end
-
- before do
- stub_ci_commit_yaml_file(YAML.dump(yaml))
- end
-
- context 'when builds are successful' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(commit.builds.pluck(:name)).to contain_exactly('build')
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
- commit.reload
- expect(commit.status).to eq('success')
- end
- end
-
- context 'when test job fails' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(commit.builds.pluck(:name)).to contain_exactly('build')
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
- commit.builds.running_or_pending.each(&:drop)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
- commit.reload
- expect(commit.status).to eq('failed')
- end
- end
-
- context 'when test and test_failure jobs fail' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(commit.builds.pluck(:name)).to contain_exactly('build')
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
- commit.builds.running_or_pending.each(&:drop)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
- commit.builds.running_or_pending.each(&:drop)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
- commit.reload
- expect(commit.status).to eq('failed')
- end
- end
-
- context 'when deploy job fails' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(commit.builds.pluck(:name)).to contain_exactly('build')
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
- commit.builds.running_or_pending.each(&:drop)
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
- commit.reload
- expect(commit.status).to eq('failed')
- end
- end
-
- context 'when build is canceled in the second stage' do
- it 'does not schedule builds after build has been canceled' do
- expect(create_builds).to be_truthy
- expect(commit.builds.pluck(:name)).to contain_exactly('build')
- expect(commit.builds.pluck(:status)).to contain_exactly('pending')
- commit.builds.running_or_pending.each(&:success)
-
- expect(commit.builds.running_or_pending).to_not be_empty
-
- expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending')
- commit.builds.running_or_pending.each(&:cancel)
-
- expect(commit.builds.running_or_pending).to be_empty
- expect(commit.reload.status).to eq('canceled')
- end
- end
- end
- end
-
- describe "#finished_at" do
- let(:commit) { FactoryGirl.create :ci_commit }
-
- it "returns finished_at of latest build" do
- build = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 60
- FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120
-
- expect(commit.finished_at.to_i).to eq(build.finished_at.to_i)
- end
-
- it "returns nil if there is no finished build" do
- FactoryGirl.create :ci_not_started_build, commit: commit
-
- expect(commit.finished_at).to be_nil
- end
- end
-
- describe "coverage" do
- let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
- let(:commit) { FactoryGirl.create :ci_commit, project: project }
-
- it "calculates average when there are two builds with coverage" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
- expect(commit.coverage).to eq("35.00")
- end
-
- it "calculates average when there are two builds with coverage and one with nil" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
- FactoryGirl.create :ci_build, commit: commit
- expect(commit.coverage).to eq("35.00")
- end
-
- it "calculates average when there are two builds with coverage and one is retried" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, commit: commit
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
- expect(commit.coverage).to eq("35.00")
- end
-
- it "calculates average when there is one build without coverage" do
- FactoryGirl.create :ci_build, commit: commit
- expect(commit.coverage).to be_nil
- end
- end
-
- describe '#retryable?' do
- subject { commit.retryable? }
-
- context 'no failed builds' do
- before do
- FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'success'
- end
-
- it 'be not retryable' do
- is_expected.to be_falsey
- end
- end
-
- context 'with failed builds' do
- before do
- FactoryGirl.create :ci_build, name: "rspec", commit: commit, status: 'running'
- FactoryGirl.create :ci_build, name: "rubocop", commit: commit, status: 'failed'
- end
-
- it 'be retryable' do
- is_expected.to be_truthy
- end
- end
- end
-
- describe '#stages' do
- let(:commit2) { FactoryGirl.create :ci_commit, project: project }
- subject { CommitStatus.where(commit: [commit, commit2]).stages }
-
- before do
- FactoryGirl.create :ci_build, commit: commit2, stage: 'test', stage_idx: 1
- FactoryGirl.create :ci_build, commit: commit, stage: 'build', stage_idx: 0
- end
-
- it 'return all stages' do
- is_expected.to eq(%w(build test))
- end
- end
-
- describe '#update_state' do
- it 'execute update_state after touching object' do
- expect(commit).to receive(:update_state).and_return(true)
- commit.touch
- end
-
- context 'dependent objects' do
- let(:commit_status) { build :commit_status, commit: commit }
-
- it 'execute update_state after saving dependent object' do
- expect(commit).to receive(:update_state).and_return(true)
- commit_status.save
- end
- end
-
- context 'update state' do
- let(:current) { Time.now.change(usec: 0) }
- let(:build) { FactoryGirl.create :ci_build, :success, commit: commit, started_at: current - 120, finished_at: current - 60 }
-
- before do
- build
- end
-
- [:status, :started_at, :finished_at, :duration].each do |param|
- it "update #{param}" do
- expect(commit.send(param)).to eq(build.send(param))
- end
- end
- end
- end
-
- describe '#branch?' do
- subject { commit.branch? }
-
- context 'is not a tag' do
- before do
- commit.tag = false
- end
-
- it 'return true when tag is set to false' do
- is_expected.to be_truthy
- end
- end
-
- context 'is not a tag' do
- before do
- commit.tag = true
- end
-
- it 'return false when tag is set to true' do
- is_expected.to be_falsey
- end
- end
- end
-end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
new file mode 100644
index 00000000000..0d769ed7324
--- /dev/null
+++ b/spec/models/ci/pipeline_spec.rb
@@ -0,0 +1,403 @@
+require 'spec_helper'
+
+describe Ci::Pipeline, models: true do
+ let(:project) { FactoryGirl.create :empty_project }
+ let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:statuses) }
+ it { is_expected.to have_many(:trigger_requests) }
+ it { is_expected.to have_many(:builds) }
+ it { is_expected.to validate_presence_of :sha }
+ it { is_expected.to validate_presence_of :status }
+
+ it { is_expected.to respond_to :git_author_name }
+ it { is_expected.to respond_to :git_author_email }
+ it { is_expected.to respond_to :short_sha }
+
+ describe :valid_commit_sha do
+ context 'commit.sha can not start with 00000000' do
+ before do
+ pipeline.sha = '0' * 40
+ pipeline.valid_commit_sha
+ end
+
+ it('commit errors should not be empty') { expect(pipeline.errors).not_to be_empty }
+ end
+ end
+
+ describe :short_sha do
+ subject { pipeline.short_sha }
+
+ it 'has 8 items' do
+ expect(subject.size).to eq(8)
+ end
+ it { expect(pipeline.sha).to start_with(subject) }
+ end
+
+ describe :create_next_builds do
+ end
+
+ describe :retried do
+ subject { pipeline.retried }
+
+ before do
+ @build1 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
+ @build2 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
+ end
+
+ it 'returns old builds' do
+ is_expected.to contain_exactly(@build1)
+ end
+ end
+
+ describe :create_builds do
+ let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false }
+
+ def create_builds(trigger_request = nil)
+ pipeline.create_builds(nil, trigger_request)
+ end
+
+ def create_next_builds
+ pipeline.create_next_builds(pipeline.builds.order(:id).last)
+ end
+
+ it 'creates builds' do
+ expect(create_builds).to be_truthy
+ pipeline.builds.update_all(status: "success")
+ expect(pipeline.builds.count(:all)).to eq(2)
+
+ expect(create_next_builds).to be_truthy
+ pipeline.builds.update_all(status: "success")
+ expect(pipeline.builds.count(:all)).to eq(4)
+
+ expect(create_next_builds).to be_truthy
+ pipeline.builds.update_all(status: "success")
+ expect(pipeline.builds.count(:all)).to eq(5)
+
+ expect(create_next_builds).to be_falsey
+ end
+
+ context 'custom stage with first job allowed to fail' do
+ let(:yaml) do
+ {
+ stages: ['clean', 'test'],
+ clean_job: {
+ stage: 'clean',
+ allow_failure: true,
+ script: 'BUILD',
+ },
+ test_job: {
+ stage: 'test',
+ script: 'TEST',
+ },
+ }
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(yaml))
+ create_builds
+ end
+
+ it 'properly schedules builds' do
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending', 'failed')
+ end
+ end
+
+ context 'properly creates builds when "when" is defined' do
+ let(:yaml) do
+ {
+ stages: ["build", "test", "test_failure", "deploy", "cleanup"],
+ build: {
+ stage: "build",
+ script: "BUILD",
+ },
+ test: {
+ stage: "test",
+ script: "TEST",
+ },
+ test_failure: {
+ stage: "test_failure",
+ script: "ON test failure",
+ when: "on_failure",
+ },
+ deploy: {
+ stage: "deploy",
+ script: "PUBLISH",
+ },
+ cleanup: {
+ stage: "cleanup",
+ script: "TIDY UP",
+ when: "always",
+ }
+ }
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(yaml))
+ end
+
+ context 'when builds are successful' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('success')
+ end
+ end
+
+ context 'when test job fails' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('failed')
+ end
+ end
+
+ context 'when test and test_failure jobs fail' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('failed')
+ end
+ end
+
+ context 'when deploy job fails' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('failed')
+ end
+ end
+
+ context 'when build is canceled in the second stage' do
+ it 'does not schedule builds after build has been canceled' do
+ expect(create_builds).to be_truthy
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(pipeline.builds.running_or_pending).not_to be_empty
+
+ expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:cancel)
+
+ expect(pipeline.builds.running_or_pending).to be_empty
+ expect(pipeline.reload.status).to eq('canceled')
+ end
+ end
+ end
+ end
+
+ describe "#finished_at" do
+ let(:pipeline) { FactoryGirl.create :ci_pipeline }
+
+ it "returns finished_at of latest build" do
+ build = FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 60
+ FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 120
+
+ expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i)
+ end
+
+ it "returns nil if there is no finished build" do
+ FactoryGirl.create :ci_not_started_build, pipeline: pipeline
+
+ expect(pipeline.finished_at).to be_nil
+ end
+ end
+
+ describe "coverage" do
+ let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
+ let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+
+ it "calculates average when there are two builds with coverage" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ expect(pipeline.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there are two builds with coverage and one with nil" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ FactoryGirl.create :ci_build, pipeline: pipeline
+ expect(pipeline.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there are two builds with coverage and one is retried" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ expect(pipeline.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there is one build without coverage" do
+ FactoryGirl.create :ci_build, pipeline: pipeline
+ expect(pipeline.coverage).to be_nil
+ end
+ end
+
+ describe '#retryable?' do
+ subject { pipeline.retryable? }
+
+ context 'no failed builds' do
+ before do
+ FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success'
+ end
+
+ it 'be not retryable' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'with failed builds' do
+ before do
+ FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running'
+ FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed'
+ end
+
+ it 'be retryable' do
+ is_expected.to be_truthy
+ end
+ end
+ end
+
+ describe '#stages' do
+ let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project }
+ subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages }
+
+ before do
+ FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1
+ FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0
+ end
+
+ it 'return all stages' do
+ is_expected.to eq(%w(build test))
+ end
+ end
+
+ describe '#update_state' do
+ it 'execute update_state after touching object' do
+ expect(pipeline).to receive(:update_state).and_return(true)
+ pipeline.touch
+ end
+
+ context 'dependent objects' do
+ let(:commit_status) { build :commit_status, pipeline: pipeline }
+
+ it 'execute update_state after saving dependent object' do
+ expect(pipeline).to receive(:update_state).and_return(true)
+ commit_status.save
+ end
+ end
+
+ context 'update state' do
+ let(:current) { Time.now.change(usec: 0) }
+ let(:build) { FactoryGirl.create :ci_build, :success, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 }
+
+ before do
+ build
+ end
+
+ [:status, :started_at, :finished_at, :duration].each do |param|
+ it "update #{param}" do
+ expect(pipeline.send(param)).to eq(build.send(param))
+ end
+ end
+ end
+ end
+
+ describe '#branch?' do
+ subject { pipeline.branch? }
+
+ context 'is not a tag' do
+ before do
+ pipeline.tag = false
+ end
+
+ it 'return true when tag is set to false' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'is not a tag' do
+ before do
+ pipeline.tag = true
+ end
+
+ it 'return false when tag is set to true' do
+ is_expected.to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
deleted file mode 100644
index 95fc160b238..00000000000
--- a/spec/models/ci/runner_project_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'spec_helper'
-
-describe Ci::RunnerProject, models: true do
- pending "add some examples to (or delete) #{__FILE__}"
-end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index eaa94228922..5d04d8ffcff 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -1,6 +1,24 @@
require 'spec_helper'
describe Ci::Runner, models: true do
+ describe 'validation' do
+ context 'when runner is not allowed to pick untagged jobs' do
+ context 'when runner does not have tags' do
+ it 'is not valid' do
+ runner = build(:ci_runner, tag_list: [], run_untagged: false)
+ expect(runner).to be_invalid
+ end
+ end
+
+ context 'when runner has tags' do
+ it 'is valid' do
+ runner = build(:ci_runner, tag_list: ['tag'], run_untagged: false)
+ expect(runner).to be_valid
+ end
+ end
+ end
+ end
+
describe '#display_name' do
it 'should return the description if it has a value' do
runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
@@ -114,7 +132,19 @@ describe Ci::Runner, models: true do
end
end
- describe '#search' do
+ describe '#has_tags?' do
+ context 'when runner has tags' do
+ subject { create(:ci_runner, tag_list: ['tag']) }
+ it { is_expected.to have_tags }
+ end
+
+ context 'when runner does not have tags' do
+ subject { create(:ci_runner, tag_list: []) }
+ it { is_expected.not_to have_tags }
+ end
+ end
+
+ describe '.search' do
let(:runner) { create(:ci_runner, token: '123abc') }
it 'returns runners with a matching token' do
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index c712d211b0f..98f60087cf5 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -23,7 +23,7 @@ describe Ci::Variable, models: true do
end
it 'fails to decrypt if iv is incorrect' do
- subject.encrypted_value_iv = nil
+ subject.encrypted_value_iv = SecureRandom.hex
subject.instance_variable_set(:@value, nil)
expect { subject.value }.
to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 9307d97e214..384a38ebc69 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -24,6 +24,16 @@ describe CommitRange, models: true do
expect { described_class.new("Foo", project) }.to raise_error(ArgumentError)
end
+ describe '#initialize' do
+ it 'does not modify strings in-place' do
+ input = "#{sha_from}...#{sha_to} "
+
+ described_class.new(input, project)
+
+ expect(input).to eq("#{sha_from}...#{sha_to} ")
+ end
+ end
+
describe '#to_s' do
it 'is correct for three-dot syntax' do
expect(range.to_s).to eq "#{full_sha_from}...#{full_sha_to}"
@@ -135,4 +145,28 @@ describe CommitRange, models: true do
end
end
end
+
+ describe '#has_been_reverted?' do
+ it 'returns true if the commit has been reverted' do
+ issue = create(:issue)
+
+ create(:note_on_issue,
+ noteable: issue,
+ system: true,
+ note: commit1.revert_description,
+ project: issue.project)
+
+ expect_any_instance_of(Commit).to receive(:reverts_commit?).
+ with(commit1).
+ and_return(true)
+
+ expect(commit1.has_been_reverted?(nil, issue)).to eq(true)
+ end
+
+ it 'returns false a commit has not been reverted' do
+ issue = create(:issue)
+
+ expect(commit1.has_been_reverted?(nil, issue)).to eq(false)
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index ccb100cd96f..beca8708c9d 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Commit, models: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
let(:commit) { project.commit }
describe 'modules' do
@@ -171,4 +171,40 @@ eos
describe '#status' do
# TODO: kamil
end
+
+ describe '#participants' do
+ let(:user1) { build(:user) }
+ let(:user2) { build(:user) }
+
+ let!(:note1) do
+ create(:note_on_commit,
+ commit_id: commit.id,
+ project: project,
+ note: 'foo')
+ end
+
+ let!(:note2) do
+ create(:note_on_commit,
+ commit_id: commit.id,
+ project: project,
+ note: 'bar')
+ end
+
+ before do
+ allow(commit).to receive(:author).and_return(user1)
+ allow(commit).to receive(:committer).and_return(user2)
+ end
+
+ it 'includes the commit author' do
+ expect(commit.participants).to include(commit.author)
+ end
+
+ it 'includes the committer' do
+ expect(commit.participants).to include(commit.committer)
+ end
+
+ it 'includes the authors of the commit notes' do
+ expect(commit.participants).to include(note1.author, note2.author)
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 434e58cfd06..8fb605fff8a 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,18 +1,18 @@
require 'spec_helper'
describe CommitStatus, models: true do
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:commit_status) { FactoryGirl.create :commit_status, commit: commit }
+ let(:pipeline) { FactoryGirl.create :ci_pipeline }
+ let(:commit_status) { FactoryGirl.create :commit_status, pipeline: pipeline }
- it { is_expected.to belong_to(:commit) }
+ it { is_expected.to belong_to(:pipeline) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:project) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) }
- it { is_expected.to delegate_method(:sha).to(:commit) }
- it { is_expected.to delegate_method(:short_sha).to(:commit) }
+ it { is_expected.to delegate_method(:sha).to(:pipeline) }
+ it { is_expected.to delegate_method(:short_sha).to(:pipeline) }
it { is_expected.to respond_to :success? }
it { is_expected.to respond_to :failed? }
@@ -121,11 +121,11 @@ describe CommitStatus, models: true do
subject { CommitStatus.latest.order(:id) }
before do
- @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
- @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
- @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'cc', status: 'success'
- @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'bb', status: 'success'
- @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'success'
+ @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running'
+ @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending'
+ @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'cc', status: 'success'
+ @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'bb', status: 'success'
+ @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success'
end
it 'return unique statuses' do
@@ -137,11 +137,11 @@ describe CommitStatus, models: true do
subject { CommitStatus.running_or_pending.order(:id) }
before do
- @commit1 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: 'bb', status: 'running'
- @commit2 = FactoryGirl.create :commit_status, commit: commit, name: 'cc', ref: 'cc', status: 'pending'
- @commit3 = FactoryGirl.create :commit_status, commit: commit, name: 'aa', ref: nil, status: 'success'
- @commit4 = FactoryGirl.create :commit_status, commit: commit, name: 'dd', ref: nil, status: 'failed'
- @commit5 = FactoryGirl.create :commit_status, commit: commit, name: 'ee', ref: nil, status: 'canceled'
+ @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running'
+ @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending'
+ @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: nil, status: 'success'
+ @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'dd', ref: nil, status: 'failed'
+ @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled'
end
it 'return statuses that are running or pending' do
@@ -152,17 +152,17 @@ describe CommitStatus, models: true do
describe '#before_sha' do
subject { commit_status.before_sha }
- context 'when no before_sha is set for ci::commit' do
- before { commit.before_sha = nil }
+ context 'when no before_sha is set for pipeline' do
+ before { pipeline.before_sha = nil }
it 'return blank sha' do
is_expected.to eq(Gitlab::Git::BLANK_SHA)
end
end
- context 'for before_sha set for ci::commit' do
+ context 'for before_sha set for pipeline' do
let(:value) { '1234' }
- before { commit.before_sha = value }
+ before { pipeline.before_sha = value }
it 'return the set value' do
is_expected.to eq(value)
@@ -172,14 +172,14 @@ describe CommitStatus, models: true do
describe '#stages' do
before do
- FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'success'
- FactoryGirl.create :commit_status, commit: commit, stage: 'build', stage_idx: 0, status: 'failed'
- FactoryGirl.create :commit_status, commit: commit, stage: 'deploy', stage_idx: 2, status: 'running'
- FactoryGirl.create :commit_status, commit: commit, stage: 'test', stage_idx: 1, status: 'success'
+ FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'success'
+ FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'failed'
+ FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'deploy', stage_idx: 2, status: 'running'
+ FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'test', stage_idx: 1, status: 'success'
end
context 'stages list' do
- subject { CommitStatus.where(commit: commit).stages }
+ subject { CommitStatus.where(pipeline: pipeline).stages }
it 'return ordered list of stages' do
is_expected.to eq(%w(build test deploy))
@@ -187,7 +187,7 @@ describe CommitStatus, models: true do
end
context 'stages with statuses' do
- subject { CommitStatus.where(commit: commit).stages_status }
+ subject { CommitStatus.where(pipeline: pipeline).stages_status }
it 'return list of stages with statuses' do
is_expected.to eq({
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
new file mode 100644
index 00000000000..98307876962
--- /dev/null
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe AccessRequestable do
+ describe 'Group' do
+ describe '#request_access' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ it { expect(group.request_access(user)).to be_a(GroupMember) }
+ it { expect(group.request_access(user).user).to eq(user) }
+ end
+
+ describe '#access_requested?' do
+ let(:group) { create(:group, :public) }
+ let(:user) { create(:user) }
+
+ before { group.request_access(user) }
+
+ it { expect(group.members.request.exists?(user_id: user)).to be_truthy }
+ end
+ end
+
+ describe 'Project' do
+ describe '#request_access' do
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ it { expect(project.request_access(user)).to be_a(ProjectMember) }
+ end
+
+ describe '#access_requested?' do
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ before { project.request_access(user) }
+
+ it { expect(project.members.request.exists?(user_id: user)).to be_truthy }
+ end
+ end
+end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
new file mode 100644
index 00000000000..a371c4a18a9
--- /dev/null
+++ b/spec/models/concerns/awardable_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Issue, "Awardable" do
+ let!(:issue) { create(:issue) }
+ let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) }
+
+ describe "Associations" do
+ it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
+ end
+
+ describe "ClassMethods" do
+ let!(:issue2) { create(:issue) }
+
+ before do
+ create(:award_emoji, awardable: issue2)
+ end
+
+ it "orders on upvotes" do
+ expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue]
+ end
+
+ it "orders on downvotes" do
+ expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2]
+ end
+ end
+
+ describe "#upvotes" do
+ it "counts the number of upvotes" do
+ expect(issue.upvotes).to be 0
+ end
+ end
+
+ describe "#downvotes" do
+ it "counts the number of downvotes" do
+ expect(issue.downvotes).to be 1
+ end
+ end
+
+ describe "#toggle_award_emoji" do
+ it "adds an emoji if it isn't awarded yet" do
+ expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1)
+ end
+
+ it "toggles already awarded emoji" do
+ expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1)
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4a4cd093435..efbcbf72f76 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -10,6 +10,20 @@ describe Issue, "Issuable" do
it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
+
+ context 'Notes' do
+ let!(:note) { create(:note, noteable: issue, project: issue.project) }
+ let(:scoped_issue) { Issue.includes(notes: :author).find(issue.id) }
+
+ it 'indicates if the notes have their authors loaded' do
+ expect(issue.notes).not_to be_authors_loaded
+ expect(scoped_issue.notes).to be_authors_loaded
+ end
+ end
+ end
+
+ describe 'Included modules' do
+ it { is_expected.to include_module(Awardable) }
end
describe "Validation" do
@@ -114,6 +128,35 @@ describe Issue, "Issuable" do
end
end
+ describe "#sort" do
+ let(:project) { build_stubbed(:empty_project) }
+
+ context "by milestone due date" do
+ # Correct order is:
+ # Issues/MRs with milestones ordered by date
+ # Issues/MRs with milestones without dates
+ # Issues/MRs without milestones
+
+ let!(:issue) { create(:issue, project: project) }
+ let!(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) }
+ let!(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) }
+ let!(:issue1) { create(:issue, project: project, milestone: early_milestone) }
+ let!(:issue2) { create(:issue, project: project, milestone: late_milestone) }
+ let!(:issue3) { create(:issue, project: project) }
+
+ it "sorts desc" do
+ issues = project.issues.sort('milestone_due_desc')
+ expect(issues).to match_array([issue2, issue1, issue, issue3])
+ end
+
+ it "sorts asc" do
+ issues = project.issues.sort('milestone_due_asc')
+ expect(issues).to match_array([issue1, issue2, issue, issue3])
+ end
+ end
+ end
+
+
describe '#subscribed?' do
context 'user is not a participant in the issue' do
before { allow(issue).to receive(:participants).with(user).and_return([]) }
@@ -160,12 +203,11 @@ describe Issue, "Issuable" do
let(:data) { issue.to_hook_data(user) }
let(:project) { issue.project }
-
it "returns correct hook data" do
expect(data[:object_kind]).to eq("issue")
expect(data[:user]).to eq(user.hook_attrs)
expect(data[:object_attributes]).to eq(issue.hook_attrs)
- expect(data).to_not have_key(:assignee)
+ expect(data).not_to have_key(:assignee)
end
context "issue is assigned" do
@@ -199,12 +241,42 @@ describe Issue, "Issuable" do
end
end
+ describe '#labels_array' do
+ let(:project) { create(:project) }
+ let(:bug) { create(:label, project: project, title: 'bug') }
+ let(:issue) { create(:issue, project: project) }
+
+ before(:each) do
+ issue.labels << bug
+ end
+
+ it 'loads the association and returns it as an array' do
+ expect(issue.reload.labels_array).to eq([bug])
+ end
+ end
+
+ describe '#user_notes_count' do
+ let(:project) { create(:project) }
+ let(:issue1) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: project) }
+
+ before do
+ create_list(:note, 3, noteable: issue1, project: project)
+ create_list(:note, 6, noteable: issue2, project: project)
+ end
+
+ it 'counts the user notes' do
+ expect(issue1.user_notes_count).to be(3)
+ expect(issue2.user_notes_count).to be(6)
+ end
+ end
+
describe "votes" do
+ let(:project) { issue.project }
+
before do
- author = create :user
- project = create :empty_project
- issue.notes.awards.create!(note: "thumbsup", author: author, project: project)
- issue.notes.awards.create!(note: "thumbsdown", author: author, project: project)
+ create(:award_emoji, :upvote, awardable: issue)
+ create(:award_emoji, :downvote, awardable: issue)
end
it "returns correct values" do
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 47c3be673c5..7e9ab8940cf 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -5,6 +5,7 @@ describe Milestone, 'Milestoneish' do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
@@ -21,6 +22,7 @@ describe Milestone, 'Milestoneish' do
before do
project.team << [member, :developer]
+ project.team << [guest, :guest]
end
describe '#closed_items_count' do
@@ -28,6 +30,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.closed_items_count(non_member)).to eq 2
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.closed_items_count(guest)).to eq 2
+ end
+
it 'should count confidential issues for author' do
expect(milestone.closed_items_count(author)).to eq 4
end
@@ -50,6 +56,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.total_items_count(non_member)).to eq 4
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.total_items_count(guest)).to eq 4
+ end
+
it 'should count confidential issues for author' do
expect(milestone.total_items_count(author)).to eq 7
end
@@ -85,6 +95,10 @@ describe Milestone, 'Milestoneish' do
expect(milestone.percent_complete(non_member)).to eq 50
end
+ it 'should not count confidential issues for project members with guest role' do
+ expect(milestone.percent_complete(guest)).to eq 50
+ end
+
it 'should count confidential issues for author' do
expect(milestone.percent_complete(author)).to eq 57
end
diff --git a/spec/models/concerns/participable_spec.rb b/spec/models/concerns/participable_spec.rb
new file mode 100644
index 00000000000..7e4ea0f2d66
--- /dev/null
+++ b/spec/models/concerns/participable_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+
+describe Participable, models: true do
+ let(:model) do
+ Class.new do
+ include Participable
+ end
+ end
+
+ describe '.participant' do
+ it 'adds the participant attributes to the existing list' do
+ model.participant(:foo)
+ model.participant(:bar)
+
+ expect(model.participant_attrs).to eq([:foo, :bar])
+ end
+ end
+
+ describe '#participants' do
+ it 'returns the list of participants' do
+ model.participant(:foo)
+ model.participant(:bar)
+
+ user1 = build(:user)
+ user2 = build(:user)
+ user3 = build(:user)
+ project = build(:project, :public)
+ instance = model.new
+
+ expect(instance).to receive(:foo).and_return(user2)
+ expect(instance).to receive(:bar).and_return(user3)
+ expect(instance).to receive(:project).twice.and_return(project)
+
+ participants = instance.participants(user1)
+
+ expect(participants).to include(user2)
+ expect(participants).to include(user3)
+ end
+
+ it 'supports attributes returning another Participable' do
+ other_model = Class.new { include Participable }
+
+ other_model.participant(:bar)
+ model.participant(:foo)
+
+ instance = model.new
+ other = other_model.new
+ user1 = build(:user)
+ user2 = build(:user)
+ project = build(:project, :public)
+
+ expect(instance).to receive(:foo).and_return(other)
+ expect(other).to receive(:bar).and_return(user2)
+ expect(instance).to receive(:project).twice.and_return(project)
+
+ expect(instance.participants(user1)).to eq([user2])
+ end
+
+ context 'when using a Proc as an attribute' do
+ it 'calls the supplied Proc' do
+ user1 = build(:user)
+ project = build(:project, :public)
+
+ user_arg = nil
+ ext_arg = nil
+
+ model.participant -> (user, ext) do
+ user_arg = user
+ ext_arg = ext
+ end
+
+ instance = model.new
+
+ expect(instance).to receive(:project).twice.and_return(project)
+
+ instance.participants(user1)
+
+ expect(user_arg).to eq(user1)
+ expect(ext_arg).to be_an_instance_of(Gitlab::ReferenceExtractor)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 30c0a04b840..9e8ebc56a31 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -28,14 +28,14 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
context 'token is not generated yet' do
describe 'token field accessor' do
subject { described_class.new.send(token_field) }
- it { is_expected.to_not be_blank }
+ it { is_expected.not_to be_blank }
end
describe 'ensured token' do
subject { described_class.new.send("ensure_#{token_field}") }
it { is_expected.to be_a String }
- it { is_expected.to_not be_blank }
+ it { is_expected.not_to be_blank }
end
describe 'ensured! token' do
@@ -49,7 +49,7 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
context 'token is generated' do
before { subject.send("reset_#{token_field}!") }
- it 'persists a new token 'do
+ it 'persists a new token' do
expect(subject.send(:read_attribute, token_field)).to be_a String
end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index b0e76fec693..166a1dc4ddb 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -50,6 +50,7 @@ describe Event, models: true do
let(:project) { create(:empty_project, :public) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:user) }
let(:admin) { create(:admin) }
@@ -61,6 +62,7 @@ describe Event, models: true do
before do
project.team << [member, :developer]
+ project.team << [guest, :guest]
end
context 'issue event' do
@@ -71,6 +73,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq true }
it { expect(event.visible_to_user?(admin)).to eq true }
end
@@ -81,6 +84,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq false }
it { expect(event.visible_to_user?(admin)).to eq true }
end
end
@@ -93,6 +97,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq true }
it { expect(event.visible_to_user?(admin)).to eq true }
end
@@ -103,6 +108,7 @@ describe Event, models: true do
it { expect(event.visible_to_user?(author)).to eq true }
it { expect(event.visible_to_user?(assignee)).to eq true }
it { expect(event.visible_to_user?(member)).to eq true }
+ it { expect(event.visible_to_user?(guest)).to eq false }
it { expect(event.visible_to_user?(admin)).to eq true }
end
end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index 0caf5869c24..c4e781dd1dc 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe GenericCommitStatus, models: true do
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, commit: commit }
+ let(:pipeline) { FactoryGirl.create :ci_pipeline }
+ let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline }
describe :context do
subject { generic_commit_status.context }
@@ -27,13 +27,13 @@ describe GenericCommitStatus, models: true do
describe :context do
subject { generic_commit_status.context }
- it { is_expected.to_not be_nil }
+ it { is_expected.not_to be_nil }
end
describe :stage do
subject { generic_commit_status.stage }
- it { is_expected.to_not be_nil }
+ it { is_expected.not_to be_nil }
end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 6fa16be7f04..ccdcb29f773 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -5,7 +5,11 @@ describe Group, models: true do
describe 'associations' do
it { is_expected.to have_many :projects }
- it { is_expected.to have_many :group_members }
+ it { is_expected.to have_many(:group_members).dependent(:destroy) }
+ it { is_expected.to have_many(:users).through(:group_members) }
+ it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
+ it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
+ it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
end
describe 'modules' do
@@ -131,4 +135,46 @@ describe Group, models: true do
expect(described_class.search(group.path.upcase)).to eq([group])
end
end
+
+ describe '#has_owner?' do
+ before { @members = setup_group_members(group) }
+
+ it { expect(group.has_owner?(@members[:owner])).to be_truthy }
+ it { expect(group.has_owner?(@members[:master])).to be_falsey }
+ it { expect(group.has_owner?(@members[:developer])).to be_falsey }
+ it { expect(group.has_owner?(@members[:reporter])).to be_falsey }
+ it { expect(group.has_owner?(@members[:guest])).to be_falsey }
+ it { expect(group.has_owner?(@members[:requester])).to be_falsey }
+ end
+
+ describe '#has_master?' do
+ before { @members = setup_group_members(group) }
+
+ it { expect(group.has_master?(@members[:owner])).to be_falsey }
+ it { expect(group.has_master?(@members[:master])).to be_truthy }
+ it { expect(group.has_master?(@members[:developer])).to be_falsey }
+ it { expect(group.has_master?(@members[:reporter])).to be_falsey }
+ it { expect(group.has_master?(@members[:guest])).to be_falsey }
+ it { expect(group.has_master?(@members[:requester])).to be_falsey }
+ end
+
+ def setup_group_members(group)
+ members = {
+ owner: create(:user),
+ master: create(:user),
+ developer: create(:user),
+ reporter: create(:user),
+ guest: create(:user),
+ requester: create(:user)
+ }
+
+ group.add_user(members[:owner], GroupMember::OWNER)
+ group.add_user(members[:master], GroupMember::MASTER)
+ group.add_user(members[:developer], GroupMember::DEVELOPER)
+ group.add_user(members[:reporter], GroupMember::REPORTER)
+ group.add_user(members[:guest], GroupMember::GUEST)
+ group.request_access(members[:requester])
+
+ members
+ end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 8ab00c70f9d..b87d68283e6 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -192,7 +192,7 @@ describe Issue, models: true do
source_project: subject.project,
source_branch: "#{subject.iid}-branch" })
merge_request.create_cross_references!(user)
- expect(subject.referenced_merge_requests).to_not be_empty
+ expect(subject.referenced_merge_requests).not_to be_empty
expect(subject.related_branches(user)).to eq([subject.to_branch_name])
end
@@ -231,4 +231,59 @@ describe Issue, models: true do
expect(issue.to_branch_name).to match /confidential-issue\z/
end
end
+
+ describe '#participants' do
+ context 'using a public project' do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ let!(:note1) do
+ create(:note_on_issue, noteable: issue, project: project, note: 'a')
+ end
+
+ let!(:note2) do
+ create(:note_on_issue, noteable: issue, project: project, note: 'b')
+ end
+
+ it 'includes the issue author' do
+ expect(issue.participants).to include(issue.author)
+ end
+
+ it 'includes the authors of the notes' do
+ expect(issue.participants).to include(note1.author, note2.author)
+ end
+ end
+
+ context 'using a private project' do
+ it 'does not include mentioned users that do not have access to the project' do
+ project = create(:project)
+ user = create(:user)
+ issue = create(:issue, project: project)
+
+ create(:note_on_issue,
+ noteable: issue,
+ project: project,
+ note: user.to_reference)
+
+ expect(issue.participants).not_to include(user)
+ end
+ end
+ end
+
+ describe 'cached counts' do
+ it 'updates when assignees change' do
+ user1 = create(:user)
+ user2 = create(:user)
+ issue = create(:issue, assignee: user1)
+
+ expect(user1.assigned_open_issues_count).to eq(1)
+ expect(user2.assigned_open_issues_count).to eq(0)
+
+ issue.assignee = user2
+ issue.save
+
+ expect(user1.assigned_open_issues_count).to eq(0)
+ expect(user2.assigned_open_issues_count).to eq(1)
+ end
+ end
end
diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb
index 7c29bef54e4..b2d06853886 100644
--- a/spec/models/legacy_diff_note_spec.rb
+++ b/spec/models/legacy_diff_note_spec.rb
@@ -63,7 +63,9 @@ describe LegacyDiffNote, models: true do
code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
# We're persisting in order to trigger the set_diff callback
- note = create(:note_on_merge_request_diff, noteable: merge, line_code: code)
+ note = create(:note_on_merge_request_diff, noteable: merge,
+ line_code: code,
+ project: merge.source_project)
# Make sure we don't get a false positive from a guard clause
expect(note).to receive(:find_noteable_diff).and_call_original
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 6e51730eecd..3ed3202ac6c 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -55,11 +55,97 @@ describe Member, models: true do
end
end
+ describe 'Scopes & finders' do
+ before do
+ project = create(:project)
+ group = create(:group)
+ @owner_user = create(:user).tap { |u| group.add_owner(u) }
+ @owner = group.members.find_by(user_id: @owner_user.id)
+
+ @master_user = create(:user).tap { |u| project.team << [u, :master] }
+ @master = project.members.find_by(user_id: @master_user.id)
+
+ ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ @invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
+
+ accepted_invite_user = build(:user)
+ ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
+
+ requested_user = create(:user).tap { |u| project.request_access(u) }
+ @requested_member = project.members.request.find_by(user_id: requested_user.id)
+
+ accepted_request_user = create(:user).tap { |u| project.request_access(u) }
+ @accepted_request_member = project.members.request.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request }
+ end
+
+ describe '.invite' do
+ it { expect(described_class.invite).not_to include @master }
+ it { expect(described_class.invite).to include @invited_member }
+ it { expect(described_class.invite).not_to include @accepted_invite_member }
+ it { expect(described_class.invite).not_to include @requested_member }
+ it { expect(described_class.invite).not_to include @accepted_request_member }
+ end
+
+ describe '.non_invite' do
+ it { expect(described_class.non_invite).to include @master }
+ it { expect(described_class.non_invite).not_to include @invited_member }
+ it { expect(described_class.non_invite).to include @accepted_invite_member }
+ it { expect(described_class.non_invite).to include @requested_member }
+ it { expect(described_class.non_invite).to include @accepted_request_member }
+ end
+
+ describe '.request' do
+ it { expect(described_class.request).not_to include @master }
+ it { expect(described_class.request).not_to include @invited_member }
+ it { expect(described_class.request).not_to include @accepted_invite_member }
+ it { expect(described_class.request).to include @requested_member }
+ it { expect(described_class.request).not_to include @accepted_request_member }
+ end
+
+ describe '.non_request' do
+ it { expect(described_class.non_request).to include @master }
+ it { expect(described_class.non_request).to include @invited_member }
+ it { expect(described_class.non_request).to include @accepted_invite_member }
+ it { expect(described_class.non_request).not_to include @requested_member }
+ it { expect(described_class.non_request).to include @accepted_request_member }
+ end
+
+ describe '.non_pending' do
+ it { expect(described_class.non_pending).to include @master }
+ it { expect(described_class.non_pending).not_to include @invited_member }
+ it { expect(described_class.non_pending).to include @accepted_invite_member }
+ it { expect(described_class.non_pending).not_to include @requested_member }
+ it { expect(described_class.non_pending).to include @accepted_request_member }
+ end
+
+ describe '.owners_and_masters' do
+ it { expect(described_class.owners_and_masters).to include @owner }
+ it { expect(described_class.owners_and_masters).to include @master }
+ it { expect(described_class.owners_and_masters).not_to include @invited_member }
+ it { expect(described_class.owners_and_masters).not_to include @accepted_invite_member }
+ it { expect(described_class.owners_and_masters).not_to include @requested_member }
+ it { expect(described_class.owners_and_masters).not_to include @accepted_request_member }
+ end
+ end
+
describe "Delegate methods" do
it { is_expected.to respond_to(:user_name) }
it { is_expected.to respond_to(:user_email) }
end
+ describe 'Callbacks' do
+ describe 'after_destroy :post_decline_request, if: :request?' do
+ let(:member) { create(:project_member, requested_at: Time.now.utc) }
+
+ it 'calls #post_decline_request' do
+ expect(member).to receive(:post_decline_request)
+
+ member.destroy
+ end
+ end
+ end
+
describe ".add_user" do
let!(:user) { create(:user) }
let(:project) { create(:project) }
@@ -97,6 +183,44 @@ describe Member, models: true do
end
end
+ describe '#accept_request' do
+ let(:member) { create(:project_member, requested_at: Time.now.utc) }
+
+ it { expect(member.accept_request).to be_truthy }
+
+ it 'clears requested_at' do
+ member.accept_request
+
+ expect(member.requested_at).to be_nil
+ end
+
+ it 'calls #after_accept_request' do
+ expect(member).to receive(:after_accept_request)
+
+ member.accept_request
+ end
+ end
+
+ describe '#invite?' do
+ subject { create(:project_member, invite_email: "user@example.com", user: nil) }
+
+ it { is_expected.to be_invite }
+ end
+
+ describe '#request?' do
+ subject { create(:project_member, requested_at: Time.now.utc) }
+
+ it { is_expected.to be_request }
+ end
+
+ describe '#pending?' do
+ let(:invited_member) { create(:project_member, invite_email: "user@example.com", user: nil) }
+ let(:requester) { create(:project_member, requested_at: Time.now.utc) }
+
+ it { expect(invited_member).to be_invite }
+ it { expect(requester).to be_pending }
+ end
+
describe "#accept_invite!" do
let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
let(:user) { create(:user) }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 5424c9b9cba..eeb74a462ac 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
describe GroupMember, models: true do
- context 'notification' do
+ describe 'notifications' do
describe "#after_create" do
it "should send email to user" do
membership = build(:group_member)
@@ -50,5 +50,31 @@ describe GroupMember, models: true do
@group_member.update_attribute(:access_level, GroupMember::OWNER)
end
end
+
+ describe '#after_accept_request' do
+ it 'calls NotificationService.accept_group_access_request' do
+ member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
+
+ expect_any_instance_of(NotificationService).to receive(:new_group_member)
+
+ member.__send__(:after_accept_request)
+ end
+ end
+
+ describe '#post_decline_request' do
+ it 'calls NotificationService.decline_group_access_request' do
+ member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
+
+ expect_any_instance_of(NotificationService).to receive(:decline_group_access_request)
+
+ member.__send__(:post_decline_request)
+ end
+ end
+
+ describe '#real_source_type' do
+ subject { create(:group_member).real_source_type }
+
+ it { is_expected.to eq 'Group' }
+ end
end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 9f26d9eb5ce..1e466f9c620 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -20,6 +20,54 @@
require 'spec_helper'
describe ProjectMember, models: true do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).class_name('Project').with_foreign_key(:source_id) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to allow_value('Project').for(:source_type) }
+ it { is_expected.not_to allow_value('project').for(:source_type) }
+ end
+
+ describe 'modules' do
+ it { is_expected.to include_module(Gitlab::ShellAdapter) }
+ end
+
+ describe '#real_source_type' do
+ subject { create(:project_member).real_source_type }
+
+ it { is_expected.to eq 'Project' }
+ end
+
+ describe "#destroy" do
+ let(:owner) { create(:project_member, access_level: ProjectMember::OWNER) }
+ let(:project) { owner.project }
+ let(:master) { create(:project_member, project: project) }
+
+ let(:owner_todos) { (0...2).map { create(:todo, user: owner.user, project: project) } }
+ let(:master_todos) { (0...3).map { create(:todo, user: master.user, project: project) } }
+
+ before do
+ owner_todos
+ master_todos
+ end
+
+ it "destroy itself and delete associated todos" do
+ expect(owner.user.todos.size).to eq(2)
+ expect(master.user.todos.size).to eq(3)
+ expect(Todo.count).to eq(5)
+
+ master_todo_ids = master_todos.map(&:id)
+ master.destroy
+
+ expect(owner.user.todos.size).to eq(2)
+ expect(Todo.count).to eq(2)
+ master_todo_ids.each do |id|
+ expect(Todo.exists?(id)).to eq(false)
+ end
+ end
+ end
+
describe :import_team do
before do
@abilities = Six.new
@@ -93,4 +141,26 @@ describe ProjectMember, models: true do
it { expect(@project_1.users).to be_empty }
it { expect(@project_2.users).to be_empty }
end
+
+ describe 'notifications' do
+ describe '#after_accept_request' do
+ it 'calls NotificationService.new_project_member' do
+ member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
+
+ expect_any_instance_of(NotificationService).to receive(:new_project_member)
+
+ member.__send__(:after_accept_request)
+ end
+ end
+
+ describe '#post_decline_request' do
+ it 'calls NotificationService.decline_project_access_request' do
+ member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
+
+ expect_any_instance_of(NotificationService).to receive(:decline_project_access_request)
+
+ member.__send__(:post_decline_request)
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 9eef08c6d00..3b199f4d98d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -119,7 +119,8 @@ describe MergeRequest, models: true do
before do
allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] }
- create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit', project: merge_request.project)
+ create(:note_on_commit, commit_id: merge_request.commits.first.id,
+ project: merge_request.project)
create(:note, noteable: merge_request, project: merge_request.project)
end
@@ -129,7 +130,9 @@ describe MergeRequest, models: true do
end
it "should include notes for commits from target project as well" do
- create(:note, commit_id: merge_request.commits.first.id, noteable_type: 'Commit', project: merge_request.target_project)
+ create(:note_on_commit, commit_id: merge_request.commits.first.id,
+ project: merge_request.target_project)
+
expect(merge_request.commits).not_to be_empty
expect(merge_request.mr_and_commit_notes.count).to eq(3)
end
@@ -260,13 +263,18 @@ describe MergeRequest, models: true do
end
describe "#reset_merge_when_build_succeeds" do
- let(:merge_if_green) { create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user) }
+ let(:merge_if_green) do
+ create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user),
+ merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" }
+ end
it "sets the item to false" do
merge_if_green.reset_merge_when_build_succeeds
merge_if_green.reload
expect(merge_if_green.merge_when_build_succeeds).to be_falsey
+ expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
+ expect(merge_if_green.merge_params["commit_message"]).to be_nil
end
end
@@ -382,19 +390,19 @@ describe MergeRequest, models: true do
subject { create :merge_request, :simple }
end
- describe '#ci_commit' do
+ describe '#pipeline' do
describe 'when the source project exists' do
it 'returns the latest commit' do
- commit = double(:commit, id: '123abc')
- ci_commit = double(:ci_commit, ref: 'master')
+ commit = double(:commit, id: '123abc')
+ pipeline = double(:ci_pipeline, ref: 'master')
allow(subject).to receive(:last_commit).and_return(commit)
- expect(subject.source_project).to receive(:ci_commit).
+ expect(subject.source_project).to receive(:pipeline).
with('123abc', 'master').
- and_return(ci_commit)
+ and_return(pipeline)
- expect(subject.ci_commit).to eq(ci_commit)
+ expect(subject.pipeline).to eq(pipeline)
end
end
@@ -402,7 +410,201 @@ describe MergeRequest, models: true do
it 'returns nil' do
allow(subject).to receive(:source_project).and_return(nil)
- expect(subject.ci_commit).to be_nil
+ expect(subject.pipeline).to be_nil
+ end
+ end
+ end
+
+ describe '#participants' do
+ let(:project) { create(:project, :public) }
+
+ let(:mr) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
+
+ let!(:note1) do
+ create(:note_on_merge_request, noteable: mr, project: project, note: 'a')
+ end
+
+ let!(:note2) do
+ create(:note_on_merge_request, noteable: mr, project: project, note: 'b')
+ end
+
+ it 'includes the merge request author' do
+ expect(mr.participants).to include(mr.author)
+ end
+
+ it 'includes the authors of the notes' do
+ expect(mr.participants).to include(note1.author, note2.author)
+ end
+ end
+
+ describe 'cached counts' do
+ it 'updates when assignees change' do
+ user1 = create(:user)
+ user2 = create(:user)
+ mr = create(:merge_request, assignee: user1)
+
+ expect(user1.assigned_open_merge_request_count).to eq(1)
+ expect(user2.assigned_open_merge_request_count).to eq(0)
+
+ mr.assignee = user2
+ mr.save
+
+ expect(user1.assigned_open_merge_request_count).to eq(0)
+ expect(user2.assigned_open_merge_request_count).to eq(1)
+ end
+ end
+
+ describe '#check_if_can_be_merged' do
+ let(:project) { create(:project, only_allow_merge_if_build_succeeds: true) }
+
+ subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
+
+ context 'when it is not broken and has no conflicts' do
+ it 'is marked as mergeable' do
+ allow(subject).to receive(:broken?) { false }
+ allow(project).to receive_message_chain(:repository, :can_be_merged?) { true }
+
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
+ end
+ end
+
+ context 'when broken' do
+ before { allow(subject).to receive(:broken?) { true } }
+
+ it 'becomes unmergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ end
+ end
+
+ context 'when it has conflicts' do
+ before do
+ allow(subject).to receive(:broken?) { false }
+ allow(project).to receive_message_chain(:repository, :can_be_merged?) { false }
+ end
+
+ it 'becomes unmergeable' do
+ expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
+ end
+ end
+ end
+
+ describe '#mergeable?' do
+ let(:project) { create(:project) }
+
+ subject { create(:merge_request, source_project: project) }
+
+ it 'returns false if #mergeable_state? is false' do
+ expect(subject).to receive(:mergeable_state?) { false }
+
+ expect(subject.mergeable?).to be_falsey
+ end
+
+ it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
+ allow(subject).to receive(:mergeable_state?) { true }
+ expect(subject).to receive(:check_if_can_be_merged)
+ expect(subject).to receive(:can_be_merged?) { true }
+
+ expect(subject.mergeable?).to be_truthy
+ end
+ end
+
+ describe '#mergeable_state?' do
+ let(:project) { create(:project) }
+
+ subject { create(:merge_request, source_project: project) }
+
+ it 'checks if merge request can be merged' do
+ allow(subject).to receive(:mergeable_ci_state?) { true }
+ expect(subject).to receive(:check_if_can_be_merged)
+
+ subject.mergeable?
+ end
+
+ context 'when not open' do
+ before { subject.close }
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+
+ context 'when working in progress' do
+ before { subject.title = 'WIP MR' }
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+
+ context 'when broken' do
+ before { allow(subject).to receive(:broken?) { true } }
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+
+ context 'when failed' do
+ before { allow(subject).to receive(:broken?) { false } }
+
+ context 'when project settings restrict to merge only if build succeeds and build failed' do
+ before do
+ project.only_allow_merge_if_build_succeeds = true
+ allow(subject).to receive(:mergeable_ci_state?) { false }
+ end
+
+ it 'returns false' do
+ expect(subject.mergeable_state?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe '#mergeable_ci_state?' do
+ let(:project) { create(:empty_project, only_allow_merge_if_build_succeeds: true) }
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ subject { build(:merge_request, target_project: project) }
+
+ context 'when it is only allowed to merge when build is green' do
+ context 'and a failed pipeline is associated' do
+ before do
+ pipeline.statuses << create(:commit_status, status: 'failed', project: project)
+ allow(subject).to receive(:pipeline) { pipeline }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_falsey }
+ end
+
+ context 'when no pipeline is associated' do
+ before do
+ allow(subject).to receive(:pipeline) { nil }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
+ end
+ end
+
+ context 'when merges are not restricted to green builds' do
+ subject { build(:merge_request, target_project: build(:empty_project, only_allow_merge_if_build_succeeds: false)) }
+
+ context 'and a failed pipeline is associated' do
+ before do
+ pipeline.statuses << create(:commit_status, status: 'failed', project: project)
+ allow(subject).to receive(:pipeline) { pipeline }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
+ end
+
+ context 'when no pipeline is associated' do
+ before do
+ allow(subject).to receive(:pipeline) { nil }
+ end
+
+ it { expect(subject.mergeable_ci_state?).to be_truthy }
end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 4074f966299..4e68ac5e63a 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -70,6 +70,20 @@ describe Namespace, models: true do
allow(@namespace).to receive(:path).and_return(new_path)
expect(@namespace.move_dir).to be_truthy
end
+
+ context "when any project has container tags" do
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags('tag')
+
+ create(:empty_project, namespace: @namespace)
+
+ allow(@namespace).to receive(:path_was).and_return(@namespace.path)
+ allow(@namespace).to receive(:path).and_return('new_path')
+ end
+
+ it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') }
+ end
end
describe :rm_dir do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 5d916f0e6a6..285ab19cfaf 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -9,9 +9,47 @@ describe Note, models: true do
it { is_expected.to have_many(:todos).dependent(:destroy) }
end
+ describe 'modules' do
+ subject { described_class }
+
+ it { is_expected.to include_module(Participable) }
+ it { is_expected.to include_module(Mentionable) }
+ it { is_expected.to include_module(Awardable) }
+
+ it { is_expected.to include_module(Gitlab::CurrentSettings) }
+ end
+
describe 'validation' do
it { is_expected.to validate_presence_of(:note) }
it { is_expected.to validate_presence_of(:project) }
+
+ context 'when note is on commit' do
+ before { allow(subject).to receive(:for_commit?).and_return(true) }
+
+ it { is_expected.to validate_presence_of(:commit_id) }
+ it { is_expected.not_to validate_presence_of(:noteable_id) }
+ end
+
+ context 'when note is not on commit' do
+ before { allow(subject).to receive(:for_commit?).and_return(false) }
+
+ it { is_expected.not_to validate_presence_of(:commit_id) }
+ it { is_expected.to validate_presence_of(:noteable_id) }
+ end
+
+ context 'when noteable and note project differ' do
+ subject do
+ build(:note, noteable: build_stubbed(:issue),
+ project: build_stubbed(:project))
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when noteable and note project are the same' do
+ subject { create(:note) }
+ it { is_expected.to be_valid }
+ end
end
describe "Commit notes" do
@@ -89,12 +127,23 @@ describe Note, models: true do
end
describe "#all_references" do
- let!(:note1) { create(:note) }
- let!(:note2) { create(:note) }
+ let!(:note1) { create(:note_on_issue) }
+ let!(:note2) { create(:note_on_issue) }
it "reads the rendered note body from the cache" do
- expect(Banzai::Renderer).to receive(:render).with(note1.note, pipeline: :note, cache_key: [note1, "note"], project: note1.project)
- expect(Banzai::Renderer).to receive(:render).with(note2.note, pipeline: :note, cache_key: [note2, "note"], project: note2.project)
+ expect(Banzai::Renderer).to receive(:render).
+ with(note1.note,
+ pipeline: :note,
+ cache_key: [note1, "note"],
+ project: note1.project,
+ author: note1.author)
+
+ expect(Banzai::Renderer).to receive(:render).
+ with(note2.note,
+ pipeline: :note,
+ cache_key: [note2, "note"],
+ project: note2.project,
+ author: note2.author)
note1.all_references
note2.all_references
@@ -102,7 +151,7 @@ describe Note, models: true do
end
describe '.search' do
- let(:note) { create(:note, note: 'WoW') }
+ let(:note) { create(:note_on_issue, note: 'WoW') }
it 'returns notes with matching content' do
expect(described_class.search(note.note)).to eq([note])
@@ -111,22 +160,31 @@ describe Note, models: true do
it 'returns notes with matching content regardless of the casing' do
expect(described_class.search('WOW')).to eq([note])
end
- end
- describe '.grouped_awards' do
- before do
- create :note, note: "smile", is_award: true
- create :note, note: "smile", is_award: true
- end
+ context "confidential issues" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) }
- it "returns grouped hash of notes" do
- expect(Note.grouped_awards.keys.size).to eq(3)
- expect(Note.grouped_awards["smile"]).to match_array(Note.all)
- end
+ it "returns notes with matching content if user can see the issue" do
+ expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note])
+ end
- it "returns thumbsup and thumbsdown always" do
- expect(Note.grouped_awards["thumbsup"]).to match_array(Note.none)
- expect(Note.grouped_awards["thumbsdown"]).to match_array(Note.none)
+ it "does not return notes with matching content if user can not see the issue" do
+ user = create(:user)
+ expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
+ end
+
+ it "does not return notes with matching content for project members with guest role" do
+ user = create(:user)
+ project.team << [user, :guest]
+ expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
+ end
+
+ it "does not return notes with matching content for unauthenticated users" do
+ expect(described_class.search(confidential_note.note)).to be_empty
+ end
end
end
@@ -140,11 +198,6 @@ describe Note, models: true do
note = build(:note, system: true)
expect(note.editable?).to be_falsy
end
-
- it "returns false" do
- note = build(:note, is_award: true, note: "smiley")
- expect(note.editable?).to be_falsy
- end
end
describe "cross_reference_not_visible_for?" do
@@ -171,23 +224,6 @@ describe Note, models: true do
end
end
- describe "set_award!" do
- let(:merge_request) { create :merge_request }
-
- it "converts aliases to actual name" do
- note = create(:note, note: ":+1:", noteable: merge_request)
- expect(note.reload.note).to eq("thumbsup")
- end
-
- it "is not an award emoji when comment is on a diff" do
- note = create(:note_on_merge_request_diff, note: ":blowfish:", noteable: merge_request, line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2")
- note = note.reload
-
- expect(note.note).to eq(":blowfish:")
- expect(note.is_award?).to be_falsy
- end
- end
-
describe 'clear_blank_line_code!' do
it 'clears a blank line code before validation' do
note = build(:note, line_code: ' ')
@@ -195,4 +231,14 @@ describe Note, models: true do
expect { note.valid? }.to change(note, :line_code).to(nil)
end
end
+
+ describe '#participants' do
+ it 'includes the note author' do
+ project = create(:project, :public)
+ issue = create(:issue, project: project)
+ note = create(:note_on_issue, noteable: issue, project: project)
+
+ expect(note.participants).to include(note.author)
+ end
+ end
end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 295081e9da1..4e24e89b008 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe NotificationSetting, type: :model do
subject { NotificationSetting.new(source_id: 1, source_type: 'Project') }
it { is_expected.to validate_presence_of(:user) }
- it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:level) }
it { is_expected.to validate_uniqueness_of(:user_id).scoped_to([:source_id, :source_type]).with_message(/already exists in source/) }
end
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index e771f35811e..9ae461f8c2d 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -126,25 +126,25 @@ describe BambooService, models: true do
it 'returns a specific URL when status is 500' do
stub_request(status: 500)
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
end
it 'returns a specific URL when response has no results' do
stub_request(body: %Q({"results":{"results":{"size":"0"}}}))
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/browse/foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo')
end
it 'returns a build URL when bamboo_url has no trailing slash' do
stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
- expect(service(bamboo_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
+ expect(service(bamboo_url: 'http://gitlab.com/bamboo').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
end
it 'returns a build URL when bamboo_url has a trailing slash' do
stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}}))
- expect(service(bamboo_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/browse/42')
+ expect(service(bamboo_url: 'http://gitlab.com/bamboo/').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42')
end
end
@@ -192,9 +192,9 @@ describe BambooService, models: true do
end
end
- def service(bamboo_url: 'http://gitlab.com')
+ def service(bamboo_url: 'http://gitlab.com/bamboo')
described_class.create(
- project: build_stubbed(:empty_project),
+ project: create(:empty_project),
properties: {
bamboo_url: bamboo_url,
username: 'mic',
@@ -205,7 +205,7 @@ describe BambooService, models: true do
end
def stub_request(status: 200, body: nil, build_state: 'success')
- bamboo_full_url = 'http://mic:password@gitlab.com/rest/api/latest/result?label=123&os_authType=basic'
+ bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic'
body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}})
WebMock.stub_request(:get, bamboo_full_url).to_return(
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 6fb5cad5011..5f618322aab 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -176,86 +176,117 @@ describe HipchatService, models: true do
context "Note events" do
let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id) }
- let(:issue) { create(:issue, project: project) }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let(:snippet) { create(:project_snippet, project: project) }
- let(:commit_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
- let(:merge_request_note) { create(:note_on_merge_request, noteable_id: merge_request.id, note: "merge request note") }
- let(:issue_note) { create(:note_on_issue, noteable_id: issue.id, note: "issue note")}
- let(:snippet_note) { create(:note_on_project_snippet, noteable_id: snippet.id, note: "snippet note") }
-
- it "should call Hipchat API for commit comment events" do
- data = Gitlab::NoteDataBuilder.build(commit_note, user)
- hipchat.execute(data)
- expect(WebMock).to have_requested(:post, api_url).once
+ context 'when commit comment event triggered' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user, project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
+
+ it "should call Hipchat API for commit comment events" do
+ data = Gitlab::NoteDataBuilder.build(commit_note, user)
+ hipchat.execute(data)
- message = hipchat.send(:create_message, data)
+ expect(WebMock).to have_requested(:post, api_url).once
- obj_attr = data[:object_attributes]
- commit_id = Commit.truncate_sha(data[:commit][:id])
- title = hipchat.send(:format_title, data[:commit][:message])
+ message = hipchat.send(:create_message, data)
- expect(message).to eq("#{user.name} commented on " \
- "<a href=\"#{obj_attr[:url]}\">commit #{commit_id}</a> in " \
- "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
- "#{title}" \
- "<pre>a comment on a commit</pre>")
+ obj_attr = data[:object_attributes]
+ commit_id = Commit.truncate_sha(data[:commit][:id])
+ title = hipchat.send(:format_title, data[:commit][:message])
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">commit #{commit_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "#{title}" \
+ "<pre>a comment on a commit</pre>")
+ end
end
- it "should call Hipchat API for merge request comment events" do
- data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
- hipchat.execute(data)
+ context 'when merge request comment event triggered' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project,
+ target_project: project)
+ end
- expect(WebMock).to have_requested(:post, api_url).once
+ let(:merge_request_note) do
+ create(:note_on_merge_request, noteable: merge_request,
+ project: project,
+ note: "merge request note")
+ end
- message = hipchat.send(:create_message, data)
+ it "should call Hipchat API for merge request comment events" do
+ data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+ hipchat.execute(data)
- obj_attr = data[:object_attributes]
- merge_id = data[:merge_request]['iid']
- title = data[:merge_request]['title']
+ expect(WebMock).to have_requested(:post, api_url).once
- expect(message).to eq("#{user.name} commented on " \
- "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \
- "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
- "<b>#{title}</b>" \
- "<pre>merge request note</pre>")
+ message = hipchat.send(:create_message, data)
+
+ obj_attr = data[:object_attributes]
+ merge_id = data[:merge_request]['iid']
+ title = data[:merge_request]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>merge request note</pre>")
+ end
end
- it "should call Hipchat API for issue comment events" do
- data = Gitlab::NoteDataBuilder.build(issue_note, user)
- hipchat.execute(data)
+ context 'when issue comment event triggered' do
+ let(:issue) { create(:issue, project: project) }
+ let(:issue_note) do
+ create(:note_on_issue, noteable: issue, project: project,
+ note: "issue note")
+ end
- message = hipchat.send(:create_message, data)
+ it "should call Hipchat API for issue comment events" do
+ data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ hipchat.execute(data)
- obj_attr = data[:object_attributes]
- issue_id = data[:issue]['iid']
- title = data[:issue]['title']
+ message = hipchat.send(:create_message, data)
- expect(message).to eq("#{user.name} commented on " \
- "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \
- "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
- "<b>#{title}</b>" \
- "<pre>issue note</pre>")
+ obj_attr = data[:object_attributes]
+ issue_id = data[:issue]['iid']
+ title = data[:issue]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>issue note</pre>")
+ end
end
- it "should call Hipchat API for snippet comment events" do
- data = Gitlab::NoteDataBuilder.build(snippet_note, user)
- hipchat.execute(data)
+ context 'when snippet comment event triggered' do
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:snippet_note) do
+ create(:note_on_project_snippet, noteable: snippet,
+ project: project,
+ note: "snippet note")
+ end
- expect(WebMock).to have_requested(:post, api_url).once
+ it "should call Hipchat API for snippet comment events" do
+ data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+ hipchat.execute(data)
- message = hipchat.send(:create_message, data)
+ expect(WebMock).to have_requested(:post, api_url).once
- obj_attr = data[:object_attributes]
- snippet_id = data[:snippet]['id']
- title = data[:snippet]['title']
+ message = hipchat.send(:create_message, data)
- expect(message).to eq("#{user.name} commented on " \
- "<a href=\"#{obj_attr[:url]}\">snippet ##{snippet_id}</a> in " \
- "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
- "<b>#{title}</b>" \
- "<pre>snippet note</pre>")
+ obj_attr = data[:object_attributes]
+ snippet_id = data[:snippet]['id']
+ title = data[:snippet]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">snippet ##{snippet_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>snippet note</pre>")
+ end
end
end
@@ -303,7 +334,7 @@ describe HipchatService, models: true do
it "should notify only broken" do
hipchat.notify_only_broken_builds = true
hipchat.execute(data)
- expect(WebMock).to_not have_requested(:post, api_url).once
+ expect(WebMock).not_to have_requested(:post, api_url).once
end
end
end
diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb
index 621c83c0cda..7fcfdf0eacd 100644
--- a/spec/models/project_services/slack_service/build_message_spec.rb
+++ b/spec/models/project_services/slack_service/build_message_spec.rb
@@ -15,7 +15,7 @@ describe SlackService::BuildMessage do
commit: {
status: status,
author_name: 'hacker',
- duration: 10,
+ duration: duration,
},
}
end
@@ -23,9 +23,10 @@ describe SlackService::BuildMessage do
context 'succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
-
+ let(:duration) { 10 }
+
it 'returns a message with information about succeeded build' do
- message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 second(s)'
+ message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
@@ -35,9 +36,23 @@ describe SlackService::BuildMessage do
context 'failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
+ let(:duration) { 10 }
it 'returns a message with information about failed build' do
- message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 second(s)'
+ message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds'
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
+
+ describe '#seconds_name' do
+ let(:status) { 'failed' }
+ let(:color) { 'danger' }
+ let(:duration) { 1 }
+
+ it 'returns seconds as singular when there is only one' do
+ message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb
index f648cbe2dee..0f8889bdf3c 100644
--- a/spec/models/project_services/slack_service/issue_message_spec.rb
+++ b/spec/models/project_services/slack_service/issue_message_spec.rb
@@ -25,7 +25,7 @@ describe SlackService::IssueMessage, models: true do
}
end
- let(:color) { '#345' }
+ let(:color) { '#C95823' }
context '#initialize' do
before do
@@ -40,10 +40,11 @@ describe SlackService::IssueMessage, models: true do
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
- 'Test User opened <url|issue #100> in <somewhere.com|project_name>: '\
- '*Issue title*')
+ '<somewhere.com|[project_name>] Issue opened by Test User')
expect(subject.attachments).to eq([
{
+ title: "#100 Issue title",
+ title_link: "url",
text: "issue description",
color: color,
}
@@ -56,10 +57,10 @@ describe SlackService::IssueMessage, models: true do
args[:object_attributes][:action] = 'close'
args[:object_attributes][:state] = 'closed'
end
+
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
- 'Test User closed <url|issue #100> in <somewhere.com|project_name>: '\
- '*Issue title*')
+ '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by Test User')
expect(subject.attachments).to be_empty
end
end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index a97b7560137..155f3e74e0d 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -142,13 +142,6 @@ describe SlackService, models: true do
let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id) }
- let(:issue) { create(:issue, project: project) }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let(:snippet) { create(:project_snippet, project: project) }
- let(:commit_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
- let(:merge_request_note) { create(:note_on_merge_request, noteable_id: merge_request.id, note: "merge request note") }
- let(:issue_note) { create(:note_on_issue, noteable_id: issue.id, note: "issue note")}
- let(:snippet_note) { create(:note_on_project_snippet, noteable_id: snippet.id, note: "snippet note") }
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
before do
@@ -162,32 +155,61 @@ describe SlackService, models: true do
WebMock.stub_request(:post, webhook_url)
end
- it "should call Slack API for commit comment events" do
- data = Gitlab::NoteDataBuilder.build(commit_note, user)
- slack.execute(data)
+ context 'when commit comment event executed' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
- expect(WebMock).to have_requested(:post, webhook_url).once
+ it "should call Slack API for commit comment events" do
+ data = Gitlab::NoteDataBuilder.build(commit_note, user)
+ slack.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
end
- it "should call Slack API for merge request comment events" do
- data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
- slack.execute(data)
+ context 'when merge request comment event executed' do
+ let(:merge_request_note) do
+ create(:note_on_merge_request, project: project,
+ note: "merge request note")
+ end
- expect(WebMock).to have_requested(:post, webhook_url).once
+ it "should call Slack API for merge request comment events" do
+ data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+ slack.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
end
- it "should call Slack API for issue comment events" do
- data = Gitlab::NoteDataBuilder.build(issue_note, user)
- slack.execute(data)
+ context 'when issue comment event executed' do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
- expect(WebMock).to have_requested(:post, webhook_url).once
+ it "should call Slack API for issue comment events" do
+ data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ slack.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
end
- it "should call Slack API for snippet comment events" do
- data = Gitlab::NoteDataBuilder.build(snippet_note, user)
- slack.execute(data)
+ context 'when snippet comment event executed' do
+ let(:snippet_note) do
+ create(:note_on_project_snippet, project: project,
+ note: "snippet note")
+ end
- expect(WebMock).to have_requested(:post, webhook_url).once
+ it "should call Slack API for snippet comment events" do
+ data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+ slack.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
end
end
end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index ad24b895170..474715d24c3 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -126,19 +126,19 @@ describe TeamcityService, models: true do
it 'returns a specific URL when status is 500' do
stub_request(status: 500)
- expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildTypeId=foo')
+ expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo')
end
it 'returns a build URL when teamcity_url has no trailing slash' do
stub_request(body: %Q({"build":{"id":"666"}}))
- expect(service(teamcity_url: 'http://gitlab.com').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo')
+ expect(service(teamcity_url: 'http://gitlab.com/teamcity').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
it 'returns a build URL when teamcity_url has a trailing slash' do
stub_request(body: %Q({"build":{"id":"666"}}))
- expect(service(teamcity_url: 'http://gitlab.com/').build_page('123', 'unused')).to eq('http://gitlab.com/viewLog.html?buildId=666&buildTypeId=foo')
+ expect(service(teamcity_url: 'http://gitlab.com/teamcity/').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo')
end
end
@@ -180,9 +180,9 @@ describe TeamcityService, models: true do
end
end
- def service(teamcity_url: 'http://gitlab.com')
+ def service(teamcity_url: 'http://gitlab.com/teamcity')
described_class.create(
- project: build_stubbed(:empty_project),
+ project: create(:empty_project),
properties: {
teamcity_url: teamcity_url,
username: 'mic',
@@ -193,7 +193,7 @@ describe TeamcityService, models: true do
end
def stub_request(status: 200, body: nil, build_status: 'success')
- teamcity_full_url = 'http://mic:password@gitlab.com/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
+ teamcity_full_url = 'http://mic:password@gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
WebMock.stub_request(:get, teamcity_full_url).to_return(
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f6e5b132643..30aa2b70c8d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -22,7 +22,7 @@ describe Project, models: true do
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
it { is_expected.to have_many(:commit_statuses) }
- it { is_expected.to have_many(:ci_commits) }
+ it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:runner_projects) }
it { is_expected.to have_many(:runners) }
@@ -53,14 +53,13 @@ describe Project, models: true do
it { is_expected.to validate_length_of(:path).is_within(0..255) }
it { is_expected.to validate_length_of(:description).is_within(0..2000) }
it { is_expected.to validate_presence_of(:creator) }
- it { is_expected.to validate_length_of(:issues_tracker_id).is_within(0..255) }
it { is_expected.to validate_presence_of(:namespace) }
it 'should not allow new projects beyond user limits' do
project2 = build(:project)
allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object)
expect(project2).not_to be_valid
- expect(project2.errors[:limit_reached].first).to match(/Your project limit is 0/)
+ expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/)
end
end
@@ -90,11 +89,17 @@ describe Project, models: true do
it { is_expected.to respond_to(:repo_exists?) }
it { is_expected.to respond_to(:update_merge_requests) }
it { is_expected.to respond_to(:execute_hooks) }
- it { is_expected.to respond_to(:name_with_namespace) }
it { is_expected.to respond_to(:owner) }
it { is_expected.to respond_to(:path_with_namespace) }
end
+ describe '#name_with_namespace' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(project.name_with_namespace).to eq "#{project.namespace.human_name} / #{project.name}" }
+ it { expect(project.human_name).to eq project.name_with_namespace }
+ end
+
describe '#to_reference' do
let(:project) { create(:empty_project) }
@@ -258,24 +263,66 @@ describe Project, models: true do
end
end
- describe :can_have_issues_tracker_id? do
+ describe :external_issue_tracker do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
- it 'should be true for projects with external issues tracker if issues enabled' do
- expect(ext_project.can_have_issues_tracker_id?).to be_truthy
+ context 'on existing projects with no value for has_external_issue_tracker' do
+ before(:each) do
+ project.update_column(:has_external_issue_tracker, nil)
+ ext_project.update_column(:has_external_issue_tracker, nil)
+ end
+
+ it 'updates the has_external_issue_tracker boolean' do
+ expect do
+ project.external_issue_tracker
+ end.to change { project.reload.has_external_issue_tracker }.to(false)
+
+ expect do
+ ext_project.external_issue_tracker
+ end.to change { ext_project.reload.has_external_issue_tracker }.to(true)
+ end
end
- it 'should be false for projects with internal issue tracker if issues enabled' do
- expect(project.can_have_issues_tracker_id?).to be_falsey
+ it 'returns nil and does not query services when there is no external issue tracker' do
+ project.build_missing_services
+ project.reload
+
+ expect(project).not_to receive(:services)
+
+ expect(project.external_issue_tracker).to eq(nil)
end
- it 'should be always false if issues disabled' do
- project.issues_enabled = false
- ext_project.issues_enabled = false
+ it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do
+ ext_project.reload # Factory returns a project with changed attributes
+ ext_project.build_missing_services
+ ext_project.reload
+
+ expect(ext_project).to receive(:services).once.and_call_original
- expect(project.can_have_issues_tracker_id?).to be_falsey
- expect(ext_project.can_have_issues_tracker_id?).to be_falsey
+ 2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) }
+ end
+ end
+
+ describe :cache_has_external_issue_tracker do
+ let(:project) { create(:project) }
+
+ it 'stores true if there is any external_issue_tracker' do
+ services = double(:service, external_issue_trackers: [RedmineService.new])
+ expect(project).to receive(:services).and_return(services)
+
+ expect do
+ project.cache_has_external_issue_tracker
+ end.to change { project.has_external_issue_tracker}.to(true)
+ end
+
+ it 'stores false if there is no external_issue_tracker' do
+ services = double(:service, external_issue_trackers: [])
+ expect(project).to receive(:services).and_return(services)
+
+ expect do
+ project.cache_has_external_issue_tracker
+ end.to change { project.has_external_issue_tracker}.to(false)
end
end
@@ -399,23 +446,23 @@ describe Project, models: true do
end
end
- describe :ci_commit do
+ describe :pipeline do
let(:project) { create :project }
- let(:commit) { create :ci_commit, project: project, ref: 'master' }
+ let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' }
- subject { project.ci_commit(commit.sha, 'master') }
+ subject { project.pipeline(pipeline.sha, 'master') }
- it { is_expected.to eq(commit) }
+ it { is_expected.to eq(pipeline) }
context 'return latest' do
- let(:commit2) { create :ci_commit, project: project, ref: 'master' }
+ let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' }
before do
- commit
- commit2
+ pipeline
+ pipeline2
end
- it { is_expected.to eq(commit2) }
+ it { is_expected.to eq(pipeline2) }
end
end
@@ -634,11 +681,11 @@ describe Project, models: true do
# Project#gitlab_shell returns a new instance of Gitlab::Shell on every
# call. This makes testing a bit easier.
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
- end
- it 'renames a repository' do
allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
+ end
+ it 'renames a repository' do
ns = project.namespace_dir
expect(gitlab_shell).to receive(:mv_repository).
@@ -663,6 +710,17 @@ describe Project, models: true do
project.rename_repo
end
+
+ context 'container registry with tags' do
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags('tag')
+ end
+
+ subject { project.rename_repo }
+
+ it { expect{subject}.to raise_error(Exception) }
+ end
end
describe '#expire_caches_before_rename' do
@@ -772,4 +830,113 @@ describe Project, models: true do
expect(project.protected_branch?('foo')).to eq(false)
end
end
+
+ describe '#container_registry_path_with_namespace' do
+ let(:project) { create(:empty_project, path: 'PROJECT') }
+
+ subject { project.container_registry_path_with_namespace }
+
+ it { is_expected.not_to eq(project.path_with_namespace) }
+ it { is_expected.to eq(project.path_with_namespace.downcase) }
+ end
+
+ describe '#container_registry_repository' do
+ let(:project) { create(:empty_project) }
+
+ before { stub_container_registry_config(enabled: true) }
+
+ subject { project.container_registry_repository }
+
+ it { is_expected.not_to be_nil }
+ end
+
+ describe '#container_registry_repository_url' do
+ let(:project) { create(:empty_project) }
+
+ subject { project.container_registry_repository_url }
+
+ before { stub_container_registry_config(**registry_settings) }
+
+ context 'for enabled registry' do
+ let(:registry_settings) do
+ {
+ enabled: true,
+ host_port: 'example.com',
+ }
+ end
+
+ it { is_expected.not_to be_nil }
+ end
+
+ context 'for disabled registry' do
+ let(:registry_settings) do
+ {
+ enabled: false
+ }
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#has_container_registry_tags?' do
+ let(:project) { create(:empty_project) }
+
+ subject { project.has_container_registry_tags? }
+
+ context 'for enabled registry' do
+ before { stub_container_registry_config(enabled: true) }
+
+ context 'with tags' do
+ before { stub_container_registry_tags('test', 'test2') }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when no tags' do
+ before { stub_container_registry_tags }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'for disabled registry' do
+ before { stub_container_registry_config(enabled: false) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '.where_paths_in' do
+ context 'without any paths' do
+ it 'returns an empty relation' do
+ expect(Project.where_paths_in([])).to eq([])
+ end
+ end
+
+ context 'without any valid paths' do
+ it 'returns an empty relation' do
+ expect(Project.where_paths_in(%w[foo])).to eq([])
+ end
+ end
+
+ context 'with valid paths' do
+ let!(:project1) { create(:project) }
+ let!(:project2) { create(:project) }
+
+ it 'returns the projects matching the paths' do
+ projects = Project.where_paths_in([project1.path_with_namespace,
+ project2.path_with_namespace])
+
+ expect(projects).to contain_exactly(project1, project2)
+ end
+
+ it 'returns projects regardless of the casing of paths' do
+ projects = Project.where_paths_in([project1.path_with_namespace.upcase,
+ project2.path_with_namespace.upcase])
+
+ expect(projects).to contain_exactly(project1, project2)
+ end
+ end
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index bacb17a8883..9262aeb6ed8 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -29,6 +29,9 @@ describe ProjectTeam, models: true do
it { expect(project.team.master?(nonmember)).to be_falsey }
it { expect(project.team.member?(nonmember)).to be_falsey }
it { expect(project.team.member?(guest)).to be_truthy }
+ it { expect(project.team.member?(reporter, Gitlab::Access::REPORTER)).to be_truthy }
+ it { expect(project.team.member?(guest, Gitlab::Access::REPORTER)).to be_falsey }
+ it { expect(project.team.member?(nonmember, Gitlab::Access::GUEST)).to be_falsey }
end
end
@@ -64,50 +67,48 @@ describe ProjectTeam, models: true do
it { expect(project.team.master?(nonmember)).to be_falsey }
it { expect(project.team.member?(nonmember)).to be_falsey }
it { expect(project.team.member?(guest)).to be_truthy }
+ it { expect(project.team.member?(guest, Gitlab::Access::MASTER)).to be_truthy }
+ it { expect(project.team.member?(reporter, Gitlab::Access::MASTER)).to be_falsey }
+ it { expect(project.team.member?(nonmember, Gitlab::Access::GUEST)).to be_falsey }
end
end
- describe :max_invited_level do
- let(:group) { create(:group) }
- let(:project) { create(:empty_project) }
-
- before do
- project.project_group_links.create(
- group: group,
- group_access: Gitlab::Access::DEVELOPER
- )
-
- group.add_user(master, Gitlab::Access::MASTER)
- group.add_user(reporter, Gitlab::Access::REPORTER)
- end
-
- it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) }
- it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) }
- it { expect(project.team.max_invited_level(nonmember.id)).to be_nil }
- end
-
- describe :max_member_access do
- let(:group) { create(:group) }
- let(:project) { create(:empty_project) }
-
- before do
- project.project_group_links.create(
- group: group,
- group_access: Gitlab::Access::DEVELOPER
- )
-
- group.add_user(master, Gitlab::Access::MASTER)
- group.add_user(reporter, Gitlab::Access::REPORTER)
+ describe '#find_member' do
+ context 'personal project' do
+ let(:project) { create(:empty_project) }
+ let(:requester) { create(:user) }
+
+ before do
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ project.request_access(requester)
+ end
+
+ it { expect(project.team.find_member(master.id)).to be_a(ProjectMember) }
+ it { expect(project.team.find_member(reporter.id)).to be_a(ProjectMember) }
+ it { expect(project.team.find_member(guest.id)).to be_a(ProjectMember) }
+ it { expect(project.team.find_member(nonmember.id)).to be_nil }
+ it { expect(project.team.find_member(requester.id)).to be_nil }
end
- it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
- it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
- it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
-
- it "does not have an access" do
- project.namespace.update(share_with_group_lock: true)
- expect(project.team.max_member_access(master.id)).to be_nil
- expect(project.team.max_member_access(reporter.id)).to be_nil
+ context 'group project' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+ let(:requester) { create(:user) }
+
+ before do
+ group.add_master(master)
+ group.add_reporter(reporter)
+ group.add_guest(guest)
+ group.request_access(requester)
+ end
+
+ it { expect(project.team.find_member(master.id)).to be_a(GroupMember) }
+ it { expect(project.team.find_member(reporter.id)).to be_a(GroupMember) }
+ it { expect(project.team.find_member(guest.id)).to be_a(GroupMember) }
+ it { expect(project.team.find_member(nonmember.id)).to be_nil }
+ it { expect(project.team.find_member(requester.id)).to be_nil }
end
end
@@ -132,4 +133,69 @@ describe ProjectTeam, models: true do
expect(project.team.human_max_access(user.id)).to eq 'Owner'
end
end
+
+ describe '#max_member_access' do
+ let(:requester) { create(:user) }
+
+ context 'personal project' do
+ let(:project) { create(:empty_project) }
+
+ context 'when project is not shared with group' do
+ before do
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ project.request_access(requester)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+ it { expect(project.team.max_member_access(requester.id)).to be_nil }
+ end
+
+ context 'when project is shared with group' do
+ before do
+ group = create(:group)
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER)
+
+ group.add_master(master)
+ group.add_reporter(reporter)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+ it { expect(project.team.max_member_access(requester.id)).to be_nil }
+
+ context 'but share_with_group_lock is true' do
+ before { project.namespace.update(share_with_group_lock: true) }
+
+ it { expect(project.team.max_member_access(master.id)).to be_nil }
+ it { expect(project.team.max_member_access(reporter.id)).to be_nil }
+ end
+ end
+ end
+
+ context 'group project' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, group: group) }
+
+ before do
+ group.add_master(master)
+ group.add_reporter(reporter)
+ group.add_guest(guest)
+ group.request_access(requester)
+ end
+
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
+ it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
+ it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
+ it { expect(project.team.max_member_access(requester.id)).to be_nil }
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 91ebb612baa..58b57bd4fef 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -16,6 +16,12 @@ describe ProjectWiki, models: true do
end
end
+ describe '#web_url' do
+ it 'returns the full web URL to the wiki' do
+ expect(subject.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/wikis/home")
+ end
+ end
+
describe "#url_to_repo" do
it "returns the correct ssh url to the repo" do
expect(subject.url_to_repo).to eq(gitlab_shell.url_to_repo(subject.path_with_namespace))
@@ -257,6 +263,13 @@ describe ProjectWiki, models: true do
end
end
+ describe '#hook_attrs' do
+ it 'returns a hash with values' do
+ expect(subject.hook_attrs).to be_a Hash
+ expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch)
+ end
+ end
+
private
def create_temp_repo(path)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 34a13f9b5c9..8c2347992f1 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -100,6 +100,12 @@ describe Repository, models: true do
expect(results.first).not_to start_with('fatal:')
end
+ it 'properly handles an unmatched parenthesis' do
+ results = repository.search_files("test(", 'master')
+
+ expect(results.first).not_to start_with('fatal:')
+ end
+
describe 'result' do
subject { results.first }
@@ -176,6 +182,15 @@ describe Repository, models: true do
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end
+ it 'handles when HEAD points to non-existent ref' do
+ repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ rugged = double('rugged')
+ expect(rugged).to receive(:head_unborn?).and_return(true)
+ expect(repository).to receive(:rugged).and_return(rugged)
+
+ expect(repository.license_blob).to be_nil
+ end
+
it 'looks in the root_ref only' do
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown')
repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false)
@@ -204,6 +219,15 @@ describe Repository, models: true do
repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end
+ it 'handles when HEAD points to non-existent ref' do
+ repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ rugged = double('rugged')
+ expect(rugged).to receive(:head_unborn?).and_return(true)
+ expect(repository).to receive(:rugged).and_return(rugged)
+
+ expect(repository.license_key).to be_nil
+ end
+
it 'returns nil when no license is detected' do
expect(repository.license_key).to be_nil
end
@@ -419,7 +443,7 @@ describe Repository, models: true do
end
it 'does nothing' do
- expect(repository.raw_repository).to_not receive(:autocrlf=).
+ expect(repository.raw_repository).not_to receive(:autocrlf=).
with(:input)
repository.update_autocrlf_option
@@ -487,7 +511,7 @@ describe Repository, models: true do
it 'does not expire the emptiness caches for a non-empty repository' do
expect(repository).to receive(:empty?).and_return(false)
- expect(repository).to_not receive(:expire_emptiness_caches)
+ expect(repository).not_to receive(:expire_emptiness_caches)
repository.expire_cache
end
@@ -650,7 +674,7 @@ describe Repository, models: true do
end
it 'does not flush caches that depend on repository data' do
- expect(repository).to_not receive(:expire_cache)
+ expect(repository).not_to receive(:expire_cache)
repository.before_delete
end
@@ -805,18 +829,6 @@ describe Repository, models: true do
end
end
- describe "#main_language" do
- it 'shows the main language of the project' do
- expect(repository.main_language).to eq("Ruby")
- end
-
- it 'returns nil when the repository is empty' do
- allow(repository).to receive(:empty?).and_return(true)
-
- expect(repository.main_language).to be_nil
- end
- end
-
describe '#before_remove_tag' do
it 'flushes the tag cache' do
expect(repository).to receive(:expire_tag_count_cache)
@@ -927,7 +939,7 @@ describe Repository, models: true do
expect(repository.avatar).to eq('logo.png')
- expect(repository).to_not receive(:blob_at_branch)
+ expect(repository).not_to receive(:blob_at_branch)
expect(repository.avatar).to eq('logo.png')
end
end
@@ -1021,7 +1033,7 @@ describe Repository, models: true do
and_return(true)
repository.cache_keys.each do |key|
- expect(repository).to_not receive(key)
+ expect(repository).not_to receive(key)
end
repository.build_cache
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 8592e112c50..2f000dbc01a 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -204,4 +204,37 @@ describe Service, models: true do
expect(service.bamboo_url_was).to be_nil
end
end
+
+ describe "callbacks" do
+ let(:project) { create(:project) }
+ let!(:service) do
+ RedmineService.new(
+ project: project,
+ active: true,
+ properties: {
+ project_url: 'http://redmine/projects/project_name_in_redmine',
+ issues_url: "http://redmine/#{project.id}/project_name_in_redmine/:id",
+ new_issue_url: 'http://redmine/projects/project_name_in_redmine/issues/new'
+ }
+ )
+ end
+
+ describe "on create" do
+ it "updates the has_external_issue_tracker boolean" do
+ expect do
+ service.save!
+ end.to change { service.project.has_external_issue_tracker }.from(nil).to(true)
+ end
+ end
+
+ describe "on update" do
+ it "updates the has_external_issue_tracker boolean" do
+ service.save!
+
+ expect do
+ service.update_attributes(active: false)
+ end.to change { service.project.has_external_issue_tracker }.from(true).to(false)
+ end
+ end
+ end
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 7a613e360d4..789816bf2c7 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -87,4 +87,31 @@ describe Snippet, models: true do
expect(described_class.search_code('FOO')).to eq([snippet])
end
end
+
+ describe '#participants' do
+ let(:project) { create(:project, :public) }
+ let(:snippet) { create(:snippet, content: 'foo', project: project) }
+
+ let!(:note1) do
+ create(:note_on_project_snippet,
+ noteable: snippet,
+ project: project,
+ note: 'a')
+ end
+
+ let!(:note2) do
+ create(:note_on_project_snippet,
+ noteable: snippet,
+ project: project,
+ note: 'b')
+ end
+
+ it 'includes the snippet author' do
+ expect(snippet.participants).to include(snippet.author)
+ end
+
+ it 'includes the note authors' do
+ expect(snippet.participants).to include(note1.author, note2.author)
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 9581990666b..73bee535fe3 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -30,6 +30,7 @@ describe User, models: true do
it { is_expected.to have_one(:abuse_report) }
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
+ it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end
describe 'validations' do
@@ -67,7 +68,10 @@ describe User, models: true do
describe 'email' do
context 'when no signup domains listed' do
- before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return([]) }
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([])
+ end
+
it 'accepts any email' do
user = build(:user, email: "info@example.com")
expect(user).to be_valid
@@ -75,7 +79,10 @@ describe User, models: true do
end
context 'when a signup domain is listed and subdomains are allowed' do
- before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) }
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com'])
+ end
+
it 'accepts info@example.com' do
user = build(:user, email: "info@example.com")
expect(user).to be_valid
@@ -93,7 +100,9 @@ describe User, models: true do
end
context 'when a signup domain is listed and subdomains are not allowed' do
- before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return(['example.com']) }
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com'])
+ end
it 'accepts info@example.com' do
user = build(:user, email: "info@example.com")
@@ -120,6 +129,66 @@ describe User, models: true do
end
end
+ describe "scopes" do
+ describe ".with_two_factor" do
+ it "returns users with 2fa enabled via OTP" do
+ user_with_2fa = create(:user, :two_factor_via_otp)
+ user_without_2fa = create(:user)
+ users_with_two_factor = User.with_two_factor.pluck(:id)
+
+ expect(users_with_two_factor).to include(user_with_2fa.id)
+ expect(users_with_two_factor).not_to include(user_without_2fa.id)
+ end
+
+ it "returns users with 2fa enabled via U2F" do
+ user_with_2fa = create(:user, :two_factor_via_u2f)
+ user_without_2fa = create(:user)
+ users_with_two_factor = User.with_two_factor.pluck(:id)
+
+ expect(users_with_two_factor).to include(user_with_2fa.id)
+ expect(users_with_two_factor).not_to include(user_without_2fa.id)
+ end
+
+ it "returns users with 2fa enabled via OTP and U2F" do
+ user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
+ user_without_2fa = create(:user)
+ users_with_two_factor = User.with_two_factor.pluck(:id)
+
+ expect(users_with_two_factor).to eq([user_with_2fa.id])
+ expect(users_with_two_factor).not_to include(user_without_2fa.id)
+ end
+ end
+
+ describe ".without_two_factor" do
+ it "excludes users with 2fa enabled via OTP" do
+ user_with_2fa = create(:user, :two_factor_via_otp)
+ user_without_2fa = create(:user)
+ users_without_two_factor = User.without_two_factor.pluck(:id)
+
+ expect(users_without_two_factor).to include(user_without_2fa.id)
+ expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ end
+
+ it "excludes users with 2fa enabled via U2F" do
+ user_with_2fa = create(:user, :two_factor_via_u2f)
+ user_without_2fa = create(:user)
+ users_without_two_factor = User.without_two_factor.pluck(:id)
+
+ expect(users_without_two_factor).to include(user_without_2fa.id)
+ expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ end
+
+ it "excludes users with 2fa enabled via OTP and U2F" do
+ user_with_2fa = create(:user, :two_factor_via_otp, :two_factor_via_u2f)
+ user_without_2fa = create(:user)
+ users_without_two_factor = User.without_two_factor.pluck(:id)
+
+ expect(users_without_two_factor).to include(user_without_2fa.id)
+ expect(users_without_two_factor).not_to include(user_with_2fa.id)
+ end
+ end
+ end
+
describe "Respond to" do
it { is_expected.to respond_to(:is_admin?) }
it { is_expected.to respond_to(:name) }
@@ -141,7 +210,10 @@ describe User, models: true do
end
describe '#confirm' do
- before { allow(current_application_settings).to receive(:send_user_confirmation_email).and_return(true) }
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
+ end
+
let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'test@gitlab.com') }
it 'returns unconfirmed' do
@@ -784,6 +856,75 @@ describe User, models: true do
it { is_expected.to eq([private_project]) }
end
+ describe '#ci_authorized_runners' do
+ let(:user) { create(:user) }
+ let(:runner) { create(:ci_runner) }
+
+ before do
+ project.runners << runner
+ end
+
+ context 'without any projects' do
+ let(:project) { create(:project) }
+
+ it 'does not load' do
+ expect(user.ci_authorized_runners).to be_empty
+ end
+ end
+
+ context 'with personal projects runners' do
+ let(:namespace) { create(:namespace, owner: user) }
+ let(:project) { create(:project, namespace: namespace) }
+
+ it 'loads' do
+ expect(user.ci_authorized_runners).to contain_exactly(runner)
+ end
+ end
+
+ shared_examples :member do
+ context 'when the user is a master' do
+ before do
+ add_user(Gitlab::Access::MASTER)
+ end
+
+ it 'loads' do
+ expect(user.ci_authorized_runners).to contain_exactly(runner)
+ end
+ end
+
+ context 'when the user is a developer' do
+ before do
+ add_user(Gitlab::Access::DEVELOPER)
+ end
+
+ it 'does not load' do
+ expect(user.ci_authorized_runners).to be_empty
+ end
+ end
+ end
+
+ context 'with groups projects runners' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+
+ def add_user(access)
+ group.add_user(user, access)
+ end
+
+ it_behaves_like :member
+ end
+
+ context 'with other projects runners' do
+ let(:project) { create(:project) }
+
+ def add_user(access)
+ project.team << [user, access]
+ end
+
+ it_behaves_like :member
+ end
+ end
+
describe '#viewable_starred_projects' do
let(:user) { create(:user) }
let(:public_project) { create(:empty_project, :public) }
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 0fbc984c061..ac85f340922 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -9,8 +9,8 @@ describe API::API, api: true do
let!(:project) { create(:project, creator_id: user.id) }
let!(:developer) { create(:project_member, :developer, user: user, project: project) }
let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) }
- let(:commit) { create(:ci_commit, project: project)}
- let(:build) { create(:ci_build, commit: commit) }
+ let(:pipeline) { create(:ci_pipeline, project: project)}
+ let(:build) { create(:ci_build, pipeline: pipeline) }
describe 'GET /projects/:id/builds ' do
let(:query) { '' }
@@ -59,8 +59,8 @@ describe API::API, api: true do
describe 'GET /projects/:id/repository/commits/:sha/builds' do
before do
- project.ensure_ci_commit(commit.sha, 'master')
- get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user)
+ project.ensure_pipeline(pipeline.sha, 'master')
+ get api("/projects/#{project.id}/repository/commits/#{pipeline.sha}/builds", api_user)
end
context 'authorized user' do
@@ -102,7 +102,7 @@ describe API::API, api: true do
before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) }
context 'build with artifacts' do
- let(:build) { create(:ci_build, :artifacts, commit: commit) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
context 'authorized user' do
let(:download_headers) do
@@ -131,7 +131,7 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/builds/:build_id/trace' do
- let(:build) { create(:ci_build, :trace, commit: commit) }
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) }
@@ -181,7 +181,7 @@ describe API::API, api: true do
end
describe 'POST /projects/:id/builds/:build_id/retry' do
- let(:build) { create(:ci_build, :canceled, commit: commit) }
+ let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) }
@@ -218,7 +218,7 @@ describe API::API, api: true do
end
context 'build is erasable' do
- let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, commit: commit) }
+ let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
it 'should erase build content' do
expect(response.status).to eq 201
@@ -234,11 +234,37 @@ describe API::API, api: true do
end
context 'build is not erasable' do
- let(:build) { create(:ci_build, :trace, project: project, commit: commit) }
+ let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
it 'should respond with forbidden' do
expect(response.status).to eq 403
end
end
end
+
+ describe 'POST /projects/:id/builds/:build_id/artifacts/keep' do
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/artifacts/keep", user)
+ end
+
+ context 'artifacts did not expire' do
+ let(:build) do
+ create(:ci_build, :trace, :artifacts, :success,
+ project: project, pipeline: pipeline, artifacts_expire_at: Time.now + 7.days)
+ end
+
+ it 'keeps artifacts' do
+ expect(response.status).to eq 200
+ expect(build.reload.artifacts_expire_at).to be_nil
+ end
+ end
+
+ context 'no artifacts' do
+ let(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+
+ it 'responds with not found' do
+ expect(response.status).to eq 404
+ end
+ end
+ end
end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 633927c8c3e..298cdbad329 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -5,7 +5,7 @@ describe API::CommitStatuses, api: true do
let!(:project) { create(:project) }
let(:commit) { project.repository.commit }
- let(:commit_status) { create(:commit_status, commit: ci_commit) }
+ let(:commit_status) { create(:commit_status, pipeline: pipeline) }
let(:guest) { create_user(:guest) }
let(:reporter) { create_user(:reporter) }
let(:developer) { create_user(:developer) }
@@ -16,8 +16,8 @@ describe API::CommitStatuses, api: true do
let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" }
context 'ci commit exists' do
- let!(:master) { project.ci_commits.create(sha: commit.id, ref: 'master') }
- let!(:develop) { project.ci_commits.create(sha: commit.id, ref: 'develop') }
+ let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') }
+ let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') }
it_behaves_like 'a paginated resources' do
let(:request) { get api(get_url, reporter) }
@@ -27,7 +27,7 @@ describe API::CommitStatuses, api: true do
let(:statuses_id) { json_response.map { |status| status['id'] } }
def create_status(commit, opts = {})
- create(:commit_status, { commit: commit, ref: commit.ref }.merge(opts))
+ create(:commit_status, { pipeline: commit, ref: commit.ref }.merge(opts))
end
let!(:status1) { create_status(master, status: 'running') }
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index cb82ca7802d..6fc38f537d3 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -90,10 +90,10 @@ describe API::API, api: true do
end
it "should return status for CI" do
- ci_commit = project.ensure_ci_commit(project.repository.commit.sha, 'master')
+ pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
expect(response.status).to eq(200)
- expect(json_response['status']).to eq(ci_commit.status)
+ expect(json_response['status']).to eq(pipeline.status)
end
end
diff --git a/spec/requests/api/gitignores_spec.rb b/spec/requests/api/gitignores_spec.rb
new file mode 100644
index 00000000000..aab2d8c81b9
--- /dev/null
+++ b/spec/requests/api/gitignores_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe API::Gitignores, api: true do
+ include ApiHelpers
+
+ describe 'Entity Gitignore' do
+ before { get api('/gitignores/Ruby') }
+
+ it { expect(json_response['name']).to eq('Ruby') }
+ it { expect(json_response['content']).to include('*.gem') }
+ end
+
+ describe 'Entity GitignoresList' do
+ before { get api('/gitignores') }
+
+ it { expect(json_response.first['name']).not_to be_nil }
+ it { expect(json_response.first['content']).to be_nil }
+ end
+
+ describe 'GET /gitignores' do
+ it 'returns a list of available license templates' do
+ get api('/gitignores')
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to be > 15
+ end
+ end
+end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 37ddab83c30..7ecefce80d6 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -12,6 +12,7 @@ describe API::API, api: true do
let!(:group2) { create(:group, :private) }
let!(:project1) { create(:project, namespace: group1) }
let!(:project2) { create(:project, namespace: group2) }
+ let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
group1.add_owner(user1)
@@ -147,9 +148,11 @@ describe API::API, api: true do
context "when authenticated as user" do
it "should return the group's projects" do
get api("/groups/#{group1.id}/projects", user1)
+
expect(response.status).to eq(200)
- expect(json_response.length).to eq(1)
- expect(json_response.first['name']).to eq(project1.name)
+ expect(json_response.length).to eq(2)
+ project_names = json_response.map { |proj| proj['name' ] }
+ expect(project_names).to match_array([project1.name, project3.name])
end
it "should not return a non existing group" do
@@ -162,6 +165,16 @@ describe API::API, api: true do
expect(response.status).to eq(404)
end
+
+ it "should only return projects to which user has access" do
+ project3.team << [user3, :developer]
+
+ get api("/groups/#{group1.id}/projects", user3)
+
+ expect(response.status).to eq(200)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['name']).to eq(project3.name)
+ end
end
context "when authenticated as admin" do
@@ -181,8 +194,10 @@ describe API::API, api: true do
context 'when using group path in URL' do
it 'should return any existing group' do
get api("/groups/#{group1.path}/projects", admin)
+
expect(response.status).to eq(200)
- expect(json_response.first['name']).to eq(project1.name)
+ project_names = json_response.map { |proj| proj['name' ] }
+ expect(project_names).to match_array([project1.name, project3.name])
end
it 'should not return a non existing group' do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 37ab9cc8cfe..59e557c5b2a 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -5,6 +5,7 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
let(:author) { create(:author) }
let(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
@@ -41,7 +42,10 @@ describe API::API, api: true do
end
let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
- before { project.team << [user, :reporter] }
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
describe "GET /issues" do
context "when unauthenticated" do
@@ -144,6 +148,14 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
+ it 'should return project issues without confidential issues for project members with guest role' do
+ get api("#{base_url}/issues", guest)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
it 'should return project confidential issues for author' do
get api("#{base_url}/issues", author)
expect(response.status).to eq(200)
@@ -249,7 +261,6 @@ describe API::API, api: true do
expect(json_response['milestone']).to be_a Hash
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
- expect(json_response['user_notes_count']).to be(1)
end
it "should return a project issue by id" do
@@ -279,6 +290,11 @@ describe API::API, api: true do
expect(response.status).to eq(404)
end
+ it "should return 404 for project members with guest role" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
+ expect(response.status).to eq(404)
+ end
+
it "should return confidential issue for project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
expect(response.status).to eq(200)
@@ -414,6 +430,12 @@ describe API::API, api: true do
expect(response.status).to eq(403)
end
+ it "should return 403 for project members with guest role" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
+ title: 'updated title'
+ expect(response.status).to eq(403)
+ end
+
it "should update a confidential issue for project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
title: 'updated title'
diff --git a/spec/requests/api/licenses_spec.rb b/spec/requests/api/licenses_spec.rb
index c17dcb222a9..3726b2f5688 100644
--- a/spec/requests/api/licenses_spec.rb
+++ b/spec/requests/api/licenses_spec.rb
@@ -57,7 +57,7 @@ describe API::Licenses, api: true do
end
it 'replaces placeholder values' do
- expect(json_response['content']).to include('Copyright (c) 2016 Anton')
+ expect(json_response['content']).to include("Copyright (c) #{Time.now.year} Anton")
end
end
@@ -70,7 +70,7 @@ describe API::Licenses, api: true do
it 'replaces placeholder values' do
expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
end
end
@@ -83,7 +83,7 @@ describe API::Licenses, api: true do
it 'replaces placeholder values' do
expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
end
end
@@ -96,7 +96,7 @@ describe API::Licenses, api: true do
it 'replaces placeholder values' do
expect(json_response['content']).to include('My Awesome Project')
- expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ expect(json_response['content']).to include("Copyright (C) #{Time.now.year} Anton")
end
end
@@ -108,7 +108,7 @@ describe API::Licenses, api: true do
end
it 'replaces placeholder values' do
- expect(json_response['content']).to include('Copyright 2016 Anton')
+ expect(json_response['content']).to include("Copyright #{Time.now.year} Anton")
end
end
@@ -128,7 +128,7 @@ describe API::Licenses, api: true do
it 'replaces the copyright owner placeholder with the name of the current user' do
get api('/licenses/mit', user)
- expect(json_response['content']).to include("Copyright (c) 2016 #{user.name}")
+ expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 4b0111df149..5896b93603f 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -138,7 +138,6 @@ describe API::API, api: true do
expect(json_response['work_in_progress']).to be_falsy
expect(json_response['merge_when_build_succeeds']).to be_falsy
expect(json_response['merge_status']).to eq('can_be_merged')
- expect(json_response['user_notes_count']).to be(2)
end
it "should return merge_request" do
@@ -388,7 +387,7 @@ describe API::API, api: true do
end
describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
- let(:ci_commit) { create(:ci_commit_without_jobs) }
+ let(:pipeline) { create(:ci_pipeline_without_jobs) }
it "should return merge_request in case of success" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
@@ -420,6 +419,15 @@ describe API::API, api: true do
expect(json_response['message']).to eq('405 Method Not Allowed')
end
+ it 'returns 405 if the build failed for a merge request that requires success' do
+ allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response.status).to eq(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
it "should return 401 if user has no permissions to merge" do
user2 = create(:user)
project.team << [user2, :reporter]
@@ -428,9 +436,22 @@ describe API::API, api: true do
expect(json_response['message']).to eq('401 Unauthorized')
end
+ it "returns 409 if the SHA parameter doesn't match" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha.succ
+
+ expect(response.status).to eq(409)
+ expect(json_response['message']).to start_with('SHA does not match HEAD of source branch')
+ end
+
+ it "succeeds if the SHA parameter matches" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.source_sha
+
+ expect(response.status).to eq(200)
+ end
+
it "enables merge when build succeeds if the ci is active" do
- allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
- allow(ci_commit).to receive(:active?).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:active?).and_return(true)
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
@@ -542,6 +563,21 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
+
+ it 'handles external issues' do
+ jira_project = create(:jira_project, :public, name: 'JIR_EXT1')
+ issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
+ merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
+ merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
+
+ get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 241995041bb..0154d1c62cc 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -146,6 +146,7 @@ describe API::API, api: true do
let(:milestone) { create(:milestone, project: public_project) }
let(:issue) { create(:issue, project: public_project) }
let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+
before do
public_project.team << [user, :developer]
milestone.issues << issue << confidential_issue
@@ -160,6 +161,18 @@ describe API::API, api: true do
expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
end
+ it 'does not return confidential issues to team members with guest role' do
+ member = create(:user)
+ project.team << [member, :guest]
+
+ get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.map { |issue| issue['id'] }).to include(issue.id)
+ end
+
it 'does not return confidential issues to regular users' do
get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user))
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 49091fc0f49..beb29a68692 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace) }
+ let!(:project) { create(:project, :public, namespace: user.namespace) }
let!(:issue) { create(:issue, project: project, author: user) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
let!(:snippet) { create(:project_snippet, project: project, author: user) }
@@ -39,6 +39,7 @@ describe API::API, api: true do
context "when noteable is an Issue" do
it "should return an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
+
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(issue_note.note)
@@ -46,20 +47,33 @@ describe API::API, api: true do
it "should return a 404 error when issue id not found" do
get api("/projects/#{project.id}/issues/12345/notes", user)
+
expect(response.status).to eq(404)
end
- context "that references a private issue" do
+ context "and current user cannot view the notes" do
it "should return an empty array" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response).to be_empty
end
+ context "and issue is confidential" do
+ before { ext_issue.update_attributes(confidential: true) }
+
+ it "returns 404" do
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
context "and current user can view the note" do
it "should return an empty array" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
+
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(cross_reference_note.note)
@@ -71,6 +85,7 @@ describe API::API, api: true do
context "when noteable is a Snippet" do
it "should return an array of snippet notes" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
+
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(snippet_note.note)
@@ -78,6 +93,13 @@ describe API::API, api: true do
it "should return a 404 error when snippet id not found" do
get api("/projects/#{project.id}/snippets/42/notes", user)
+
+ expect(response.status).to eq(404)
+ end
+
+ it "returns 404 when not authorized" do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", private_user)
+
expect(response.status).to eq(404)
end
end
@@ -85,6 +107,7 @@ describe API::API, api: true do
context "when noteable is a Merge Request" do
it "should return an array of merge_requests notes" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
+
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(merge_request_note.note)
@@ -92,6 +115,13 @@ describe API::API, api: true do
it "should return a 404 error if merge request id not found" do
get api("/projects/#{project.id}/merge_requests/4444/notes", user)
+
+ expect(response.status).to eq(404)
+ end
+
+ it "returns 404 when not authorized" do
+ get api("/projects/#{project.id}/merge_requests/4444/notes", private_user)
+
expect(response.status).to eq(404)
end
end
@@ -101,24 +131,39 @@ describe API::API, api: true do
context "when noteable is an Issue" do
it "should return an issue note by id" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
+
expect(response.status).to eq(200)
expect(json_response['body']).to eq(issue_note.note)
end
it "should return a 404 error if issue note not found" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+
expect(response.status).to eq(404)
end
- context "that references a private issue" do
+ context "and current user cannot view the note" do
it "should return a 404 error" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
+
expect(response.status).to eq(404)
end
+ context "when issue is confidential" do
+ before { issue.update_attributes(confidential: true) }
+
+ it "returns 404" do
+ get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+
context "and current user can view the note" do
it "should return an issue note by id" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
+
expect(response.status).to eq(200)
expect(json_response['body']).to eq(cross_reference_note.note)
end
@@ -129,12 +174,14 @@ describe API::API, api: true do
context "when noteable is a Snippet" do
it "should return a snippet note by id" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
+
expect(response.status).to eq(200)
expect(json_response['body']).to eq(snippet_note.note)
end
it "should return a 404 error if snippet note not found" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
+
expect(response.status).to eq(404)
end
end
@@ -144,6 +191,7 @@ describe API::API, api: true do
context "when noteable is an Issue" do
it "should create a new issue note" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
+
expect(response.status).to eq(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
@@ -151,11 +199,13 @@ describe API::API, api: true do
it "should return a 400 bad request error if body not given" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
+
expect(response.status).to eq(400)
end
it "should return a 401 unauthorized error if user not authenticated" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
+
expect(response.status).to eq(401)
end
@@ -164,6 +214,7 @@ describe API::API, api: true do
creation_time = 2.weeks.ago
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
body: 'hi!', created_at: creation_time
+
expect(response.status).to eq(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
@@ -176,6 +227,7 @@ describe API::API, api: true do
context "when noteable is a Snippet" do
it "should create a new snippet note" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
+
expect(response.status).to eq(201)
expect(json_response['body']).to eq('hi!')
expect(json_response['author']['username']).to eq(user.username)
@@ -183,11 +235,13 @@ describe API::API, api: true do
it "should return a 400 bad request error if body not given" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
+
expect(response.status).to eq(400)
end
it "should return a 401 unauthorized error if user not authenticated" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
+
expect(response.status).to eq(401)
end
end
@@ -204,8 +258,8 @@ describe API::API, api: true do
body: 'Hi!'
end
- it 'responds with 500' do
- expect(response.status).to eq 500
+ it 'responds with resource not found error' do
+ expect(response.status).to eq 404
end
it 'does not create new note' do
@@ -227,6 +281,7 @@ describe API::API, api: true do
it 'should return modified note' do
put api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user), body: 'Hello!'
+
expect(response.status).to eq(200)
expect(json_response['body']).to eq('Hello!')
end
@@ -234,12 +289,14 @@ describe API::API, api: true do
it 'should return a 404 error when note id not found' do
put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
body: 'Hello!'
+
expect(response.status).to eq(404)
end
it 'should return a 400 bad request error if body not given' do
put api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
+
expect(response.status).to eq(400)
end
end
@@ -248,6 +305,7 @@ describe API::API, api: true do
it 'should return modified note' do
put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user), body: 'Hello!'
+
expect(response.status).to eq(200)
expect(json_response['body']).to eq('Hello!')
end
@@ -255,6 +313,7 @@ describe API::API, api: true do
it 'should return a 404 error when note id not found' do
put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/12345", user), body: "Hello!"
+
expect(response.status).to eq(404)
end
end
@@ -263,6 +322,7 @@ describe API::API, api: true do
it 'should return modified note' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
"notes/#{merge_request_note.id}", user), body: 'Hello!'
+
expect(response.status).to eq(200)
expect(json_response['body']).to eq('Hello!')
end
@@ -270,6 +330,7 @@ describe API::API, api: true do
it 'should return a 404 error when note id not found' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
"notes/12345", user), body: "Hello!"
+
expect(response.status).to eq(404)
end
end
diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb
index c112ca5e3ca..44b532b10e1 100644
--- a/spec/requests/api/project_members_spec.rb
+++ b/spec/requests/api/project_members_spec.rb
@@ -133,7 +133,7 @@ describe API::API, api: true do
delete api("/projects/#{project.id}/members/#{user3.id}", user)
expect do
delete api("/projects/#{project.id}/members/#{user3.id}", user)
- end.to_not change { ProjectMember.count }
+ end.not_to change { ProjectMember.count }
expect(response.status).to eq(200)
end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 3af61d4b335..73ae8ef631c 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -184,21 +184,24 @@ describe API::Runners, api: true do
description = shared_runner.description
active = shared_runner.active
- put api("/runners/#{shared_runner.id}", admin), description: "#{description}_updated", active: !active,
- tag_list: ['ruby2.1', 'pgsql', 'mysql']
+ update_runner(shared_runner.id, admin, description: "#{description}_updated",
+ active: !active,
+ tag_list: ['ruby2.1', 'pgsql', 'mysql'],
+ run_untagged: 'false')
shared_runner.reload
expect(response.status).to eq(200)
expect(shared_runner.description).to eq("#{description}_updated")
expect(shared_runner.active).to eq(!active)
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
+ expect(shared_runner.run_untagged?).to be false
end
end
context 'when runner is not shared' do
it 'should update runner' do
description = specific_runner.description
- put api("/runners/#{specific_runner.id}", admin), description: 'test'
+ update_runner(specific_runner.id, admin, description: 'test')
specific_runner.reload
expect(response.status).to eq(200)
@@ -208,10 +211,14 @@ describe API::Runners, api: true do
end
it 'should return 404 if runner does not exists' do
- put api('/runners/9999', admin), description: 'test'
+ update_runner(9999, admin, description: 'test')
expect(response.status).to eq(404)
end
+
+ def update_runner(id, user, args)
+ put api("/runners/#{id}", user), args
+ end
end
context 'authorized user' do
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index 3e676515488..94eebc48ec8 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -49,7 +49,7 @@ describe API::API, api: true do
it "should not create new hook without url" do
expect do
post api("/hooks", admin)
- end.to_not change { SystemHook.count }
+ end.not_to change { SystemHook.count }
end
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 0510b77a39b..fdd4ec6d761 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -23,7 +23,7 @@ describe API::API do
end
before do
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
context 'Handles errors' do
@@ -44,13 +44,13 @@ describe API::API do
end
context 'Have a commit' do
- let(:commit) { project.ci_commits.last }
+ let(:pipeline) { project.pipelines.last }
it 'should create builds' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
expect(response.status).to eq(201)
- commit.builds.reload
- expect(commit.builds.size).to eq(2)
+ pipeline.builds.reload
+ expect(pipeline.builds.size).to eq(2)
end
it 'should return bad request with no builds created if there\'s no commit for that ref' do
@@ -79,8 +79,8 @@ describe API::API do
it 'create trigger request with variables' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
expect(response.status).to eq(201)
- commit.builds.reload
- expect(commit.builds.first.trigger_request.variables).to eq(variables)
+ pipeline.builds.reload
+ expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 40b24c125b5..a7690f430c4 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -20,7 +20,7 @@ describe API::API, api: true do
end
context "when authenticated" do
- #These specs are written just in case API authentication is not required anymore
+ # These specs are written just in case API authentication is not required anymore
context "when public level is restricted" do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index cae4656010f..7e50bea90d1 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -7,7 +7,7 @@ describe Ci::API::API do
let(:project) { FactoryGirl.create(:empty_project) }
before do
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
describe "Builds API for runners" do
@@ -20,9 +20,9 @@ describe Ci::API::API do
describe "POST /builds/register" do
it "should start a build" do
- commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
- commit.create_builds(nil)
- build = commit.builds.first
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
+ pipeline.create_builds(nil)
+ build = pipeline.builds.first
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -38,8 +38,8 @@ describe Ci::API::API do
end
it "should return 404 error if no builds for specific runner" do
- commit = FactoryGirl.create(:ci_commit, project: shared_project)
- FactoryGirl.create(:ci_build, commit: commit, status: 'pending')
+ pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project)
+ FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
post ci_api("/builds/register"), token: runner.token
@@ -47,8 +47,8 @@ describe Ci::API::API do
end
it "should return 404 error if no builds for shared runner" do
- commit = FactoryGirl.create(:ci_commit, project: project)
- FactoryGirl.create(:ci_build, commit: commit, status: 'pending')
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project)
+ FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
post ci_api("/builds/register"), token: shared_runner.token
@@ -56,8 +56,8 @@ describe Ci::API::API do
end
it "returns options" do
- commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
- commit.create_builds(nil)
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
+ pipeline.create_builds(nil)
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -66,8 +66,8 @@ describe Ci::API::API do
end
it "returns variables" do
- commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
- commit.create_builds(nil)
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
+ pipeline.create_builds(nil)
project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -83,10 +83,10 @@ describe Ci::API::API do
it "returns variables for triggers" do
trigger = FactoryGirl.create(:ci_trigger, project: project)
- commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
- trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger)
- commit.create_builds(nil, trigger_request)
+ trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger)
+ pipeline.create_builds(nil, trigger_request)
project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -103,9 +103,9 @@ describe Ci::API::API do
end
it "returns dependent builds" do
- commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
- commit.create_builds(nil, nil)
- commit.builds.where(stage: 'test').each(&:success)
+ pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
+ pipeline.create_builds(nil, nil)
+ pipeline.builds.where(stage: 'test').each(&:success)
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -128,11 +128,43 @@ describe Ci::API::API do
end
end
end
+
+ context 'when build has no tags' do
+ before do
+ pipeline = create(:ci_pipeline, project: project)
+ create(:ci_build, pipeline: pipeline, tags: [])
+ end
+
+ context 'when runner is allowed to pick untagged builds' do
+ before { runner.update_column(:run_untagged, true) }
+
+ it 'picks build' do
+ register_builds
+
+ expect(response).to have_http_status 201
+ end
+ end
+
+ context 'when runner is not allowed to pick untagged builds' do
+ before { runner.update_column(:run_untagged, false) }
+
+ it 'does not pick build' do
+ register_builds
+
+ expect(response).to have_http_status 404
+ end
+ end
+
+ def register_builds
+ post ci_api("/builds/register"), token: runner.token,
+ info: { platform: :darwin }
+ end
+ end
end
describe "PUT /builds/:id" do
- let(:commit) {create(:ci_commit, project: project)}
- let(:build) { create(:ci_build, :trace, commit: commit, runner_id: runner.id) }
+ let(:pipeline) {create(:ci_pipeline, project: project)}
+ let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) }
before do
build.run!
@@ -205,8 +237,8 @@ describe Ci::API::API do
context "Artifacts" do
let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
- let(:commit) { create(:ci_commit, project: project) }
- let(:build) { create(:ci_build, commit: commit, runner_id: runner.id) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline, runner_id: runner.id) }
let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") }
let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
@@ -221,13 +253,13 @@ describe Ci::API::API do
it "using token as parameter" do
post authorize_url, { token: build.token }, headers
expect(response.status).to eq(200)
- expect(json_response["TempPath"]).to_not be_nil
+ expect(json_response["TempPath"]).not_to be_nil
end
it "using token as header" do
post authorize_url, {}, headers_with_token
expect(response.status).to eq(200)
- expect(json_response["TempPath"]).to_not be_nil
+ expect(json_response["TempPath"]).not_to be_nil
end
end
@@ -332,6 +364,42 @@ describe Ci::API::API do
end
end
+ context 'with an expire date' do
+ let!(:artifacts) { file_upload }
+
+ let(:post_data) do
+ { 'file.path' => artifacts.path,
+ 'file.name' => artifacts.original_filename,
+ 'expire_in' => expire_in }
+ end
+
+ before do
+ post(post_url, post_data, headers_with_token)
+ end
+
+ context 'with an expire_in given' do
+ let(:expire_in) { '7 days' }
+
+ it 'updates when specified' do
+ build.reload
+ expect(response.status).to eq(201)
+ expect(json_response['artifacts_expire_at']).not_to be_empty
+ expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days)
+ end
+ end
+
+ context 'with no expire_in given' do
+ let(:expire_in) { nil }
+
+ it 'ignores if not specified' do
+ build.reload
+ expect(response.status).to eq(201)
+ expect(json_response['artifacts_expire_at']).to be_nil
+ expect(build.artifacts_expire_at).to be_nil
+ end
+ end
+ end
+
context "artifacts file is too large" do
it "should fail to post too large artifact" do
stub_application_setting(max_artifacts_size: 0)
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index db8189ffb79..43596f07cb5 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -12,44 +12,85 @@ describe Ci::API::API do
end
describe "POST /runners/register" do
- describe "should create a runner if token provided" do
+ context 'when runner token is provided' do
before { post ci_api("/runners/register"), token: registration_token }
- it { expect(response.status).to eq(201) }
+ it 'creates runner with default values' do
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.run_untagged).to be true
+ end
end
- describe "should create a runner with description" do
- before { post ci_api("/runners/register"), token: registration_token, description: "server.hostname" }
+ context 'when runner description is provided' do
+ before do
+ post ci_api("/runners/register"), token: registration_token,
+ description: "server.hostname"
+ end
- it { expect(response.status).to eq(201) }
- it { expect(Ci::Runner.first.description).to eq("server.hostname") }
+ it 'creates runner' do
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.description).to eq("server.hostname")
+ end
end
- describe "should create a runner with tags" do
- before { post ci_api("/runners/register"), token: registration_token, tag_list: "tag1, tag2" }
+ context 'when runner tags are provided' do
+ before do
+ post ci_api("/runners/register"), token: registration_token,
+ tag_list: "tag1, tag2"
+ end
- it { expect(response.status).to eq(201) }
- it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) }
+ it 'creates runner' do
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"])
+ end
end
- describe "should create a runner if project token provided" do
+ context 'when option for running untagged jobs is provided' do
+ context 'when tags are provided' do
+ it 'creates runner' do
+ post ci_api("/runners/register"), token: registration_token,
+ run_untagged: false,
+ tag_list: ['tag']
+
+ expect(response).to have_http_status 201
+ expect(Ci::Runner.first.run_untagged).to be false
+ end
+ end
+
+ context 'when tags are not provided' do
+ it 'does not create runner' do
+ post ci_api("/runners/register"), token: registration_token,
+ run_untagged: false
+
+ expect(response).to have_http_status 404
+ end
+ end
+ end
+
+ context 'when project token is provided' do
let(:project) { FactoryGirl.create(:empty_project) }
before { post ci_api("/runners/register"), token: project.runners_token }
- it { expect(response.status).to eq(201) }
- it { expect(project.runners.size).to eq(1) }
+ it 'creates runner' do
+ expect(response).to have_http_status 201
+ expect(project.runners.size).to eq(1)
+ end
end
- it "should return 403 error if token is invalid" do
- post ci_api("/runners/register"), token: 'invalid'
+ context 'when token is invalid' do
+ it 'returns 403 error' do
+ post ci_api("/runners/register"), token: 'invalid'
- expect(response.status).to eq(403)
+ expect(response).to have_http_status 403
+ end
end
- it "should return 400 error if no token" do
- post ci_api("/runners/register")
+ context 'when no token provided' do
+ it 'returns 400 error' do
+ post ci_api("/runners/register")
- expect(response.status).to eq(400)
+ expect(response).to have_http_status 400
+ end
end
%w(name version revision platform architecture).each do |param|
@@ -60,7 +101,7 @@ describe Ci::API::API do
it do
post ci_api("/runners/register"), token: registration_token, info: { param => value }
- expect(response.status).to eq(201)
+ expect(response).to have_http_status 201
is_expected.to eq(value)
end
end
@@ -71,7 +112,7 @@ describe Ci::API::API do
let!(:runner) { FactoryGirl.create(:ci_runner) }
before { delete ci_api("/runners/delete"), token: runner.token }
- it { expect(response.status).to eq(200) }
+ it { expect(response).to have_http_status 200 }
it { expect(Ci::Runner.count).to eq(0) }
end
end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index 0ef03f9371b..72f6a3c981d 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -15,7 +15,7 @@ describe Ci::API::API do
end
before do
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
context 'Handles errors' do
@@ -36,13 +36,13 @@ describe Ci::API::API do
end
context 'Have a commit' do
- let(:commit) { project.ci_commits.last }
+ let(:pipeline) { project.pipelines.last }
it 'should create builds' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options
expect(response.status).to eq(201)
- commit.builds.reload
- expect(commit.builds.size).to eq(2)
+ pipeline.builds.reload
+ expect(pipeline.builds.size).to eq(2)
end
it 'should return bad request with no builds created if there\'s no commit for that ref' do
@@ -71,8 +71,8 @@ describe Ci::API::API do
it 'create trigger request with variables' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables)
expect(response.status).to eq(201)
- commit.builds.reload
- expect(commit.builds.first.trigger_request.variables).to eq(variables)
+ pipeline.builds.reload
+ expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
end
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
new file mode 100644
index 00000000000..c44a4a7a1fc
--- /dev/null
+++ b/spec/requests/git_http_spec.rb
@@ -0,0 +1,395 @@
+require "spec_helper"
+
+describe 'Git HTTP requests', lib: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, path: 'project.git-project') }
+
+ it "gives WWW-Authenticate hints" do
+ clone_get('doesnt/exist.git')
+
+ expect(response.header['WWW-Authenticate']).to start_with('Basic ')
+ end
+
+ context "when the project doesn't exist" do
+ context "when no authentication is provided" do
+ it "responds with status 401 (no project existence information leak)" do
+ download('doesnt/exist.git') do |response|
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ context "when username and password are provided" do
+ context "when authentication fails" do
+ it "responds with status 401" do
+ download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ context "when authentication succeeds" do
+ it "responds with status 404" do
+ download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+ end
+
+ context "when the Wiki for a project exists" do
+ it "responds with the right project" do
+ wiki = ProjectWiki.new(project)
+ project.update_attribute(:visibility_level, Project::PUBLIC)
+
+ download("/#{wiki.repository.path_with_namespace}.git") do |response|
+ json_body = ActiveSupport::JSON.decode(response.body)
+
+ expect(response.status).to eq(200)
+ expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
+ end
+ end
+ end
+
+ context "when the project exists" do
+ let(:path) { "#{project.path_with_namespace}.git" }
+
+ context "when the project is public" do
+ before do
+ project.update_attribute(:visibility_level, Project::PUBLIC)
+ end
+
+ it "downloads get status 200" do
+ download(path, {}) do |response|
+ expect(response.status).to eq(200)
+ end
+ end
+
+ it "uploads get status 401" do
+ upload(path, {}) do |response|
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context "with correct credentials" do
+ let(:env) { { user: user.username, password: user.password } }
+
+ it "uploads get status 200 (because Git hooks do the real check)" do
+ upload(path, env) do |response|
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'but git-receive-pack is disabled' do
+ it "responds with status 404" do
+ allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
+
+ upload(path, env) do |response|
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+
+ context 'but git-upload-pack is disabled' do
+ it "responds with status 404" do
+ allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
+
+ download(path, {}) do |response|
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+
+ context "when the project is private" do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
+
+ context "when no authentication is provided" do
+ it "responds with status 401 to downloads" do
+ download(path, {}) do |response|
+ expect(response.status).to eq(401)
+ end
+ end
+
+ it "responds with status 401 to uploads" do
+ upload(path, {}) do |response|
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ context "when username and password are provided" do
+ let(:env) { { user: user.username, password: 'nope' } }
+
+ context "when authentication fails" do
+ it "responds with status 401" do
+ download(path, env) do |response|
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context "when the user is IP banned" do
+ it "responds with status 401" do
+ expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
+
+ clone_get(path, env)
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ context "when authentication succeeds" do
+ let(:env) { { user: user.username, password: user.password } }
+
+ context "when the user has access to the project" do
+ before do
+ project.team << [user, :master]
+ end
+
+ context "when the user is blocked" do
+ it "responds with status 404" do
+ user.block
+ project.team << [user, :master]
+
+ download(path, env) do |response|
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ context "when the user isn't blocked" do
+ it "downloads get status 200" do
+ expect(Rack::Attack::Allow2Ban).to receive(:reset)
+
+ clone_get(path, env)
+
+ expect(response.status).to eq(200)
+ end
+
+ it "uploads get status 200" do
+ upload(path, env) do |response|
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context "when an oauth token is provided" do
+ before do
+ application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+ @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+ end
+
+ it "downloads get status 200" do
+ clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+
+ expect(response.status).to eq(200)
+ end
+
+ it "uploads get status 401 (no project existence information leak)" do
+ push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context "when blank password attempts follow a valid login" do
+ def attempt_login(include_password)
+ password = include_password ? user.password : ""
+ clone_get path, user: user.username, password: password
+ response.status
+ end
+
+ it "repeated attempts followed by successful attempt" do
+ options = Gitlab.config.rack_attack.git_basic_auth
+ maxretry = options[:maxretry] - 1
+ ip = '1.2.3.4'
+
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
+ Rack::Attack::Allow2Ban.reset(ip, options)
+
+ maxretry.times.each do
+ expect(attempt_login(false)).to eq(401)
+ end
+
+ expect(attempt_login(true)).to eq(200)
+ expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
+
+ maxretry.times.each do
+ expect(attempt_login(false)).to eq(401)
+ end
+
+ Rack::Attack::Allow2Ban.reset(ip, options)
+ end
+ end
+ end
+
+ context "when the user doesn't have access to the project" do
+ it "downloads get status 404" do
+ download(path, user: user.username, password: user.password) do |response|
+ expect(response.status).to eq(404)
+ end
+ end
+
+ it "uploads get status 200 (because Git hooks do the real check)" do
+ upload(path, user: user.username, password: user.password) do |response|
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+ end
+ end
+
+ context "when a gitlab ci token is provided" do
+ let(:token) { 123 }
+ let(:project) { FactoryGirl.create :empty_project }
+
+ before do
+ project.update_attributes(runners_token: token, builds_enabled: true)
+ end
+
+ it "downloads get status 200" do
+ clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
+
+ expect(response.status).to eq(200)
+ end
+
+ it "uploads get status 401 (no project existence information leak)" do
+ push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+ end
+
+ context "when the project path doesn't end in .git" do
+ context "GET info/refs" do
+ let(:path) { "/#{project.path_with_namespace}/info/refs" }
+
+ context "when no params are added" do
+ before { get path }
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
+ end
+ end
+
+ context "when the upload-pack service is requested" do
+ let(:params) { { service: 'git-upload-pack' } }
+ before { get path, params }
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ end
+ end
+
+ context "when the receive-pack service is requested" do
+ let(:params) { { service: 'git-receive-pack' } }
+ before { get path, params }
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ end
+ end
+
+ context "when the params are anything else" do
+ let(:params) { { service: 'git-implode-pack' } }
+ before { get path, params }
+
+ it "redirects to the sign-in page" do
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "POST git-upload-pack" do
+ it "fails to find a route" do
+ expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ end
+ end
+
+ context "POST git-receive-pack" do
+ it "failes to find a route" do
+ expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ end
+ end
+ end
+
+ context "retrieving an info/refs file" do
+ before { project.update_attribute(:visibility_level, Project::PUBLIC) }
+
+ context "when the file exists" do
+ before do
+ # Provide a dummy file in its place
+ allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
+ allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
+ Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
+ end
+
+ get "/#{project.path_with_namespace}/blob/master/info/refs"
+ end
+
+ it "returns the file" do
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when the file exists" do
+ before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
+
+ it "returns not found" do
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ def clone_get(project, options={})
+ get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password))
+ end
+
+ def clone_post(project, options={})
+ post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password))
+ end
+
+ def push_get(project, options={})
+ get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password))
+ end
+
+ def push_post(project, options={})
+ post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password))
+ end
+
+ def download(project, user: nil, password: nil)
+ args = [project, { user: user, password: password }]
+
+ clone_get(*args)
+ yield response
+
+ clone_post(*args)
+ yield response
+ end
+
+ def upload(project, user: nil, password: nil)
+ args = [project, { user: user, password: password }]
+
+ push_get(*args)
+ yield response
+
+ push_post(*args)
+ yield response
+ end
+
+ def auth_env(user, password)
+ if user && password
+ { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password) }
+ else
+ {}
+ end
+ end
+end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 7bb71365a48..d2d4a9eca18 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -23,7 +23,7 @@ describe JwtController do
context 'when using authorized request' do
context 'using CI token' do
let(:project) { create(:empty_project, runners_token: 'token', builds_enabled: builds_enabled) }
- let(:headers) { { authorization: credentials('gitlab_ci_token', project.runners_token) } }
+ let(:headers) { { authorization: credentials('gitlab-ci-token', project.runners_token) } }
subject! { get '/jwt/auth', parameters, headers }
@@ -44,7 +44,7 @@ describe JwtController do
let(:user) { create(:user) }
let(:headers) { { authorization: credentials('user', 'password') } }
- before { expect_any_instance_of(Gitlab::Auth).to receive(:find).with('user', 'password').and_return(user) }
+ before { expect(Gitlab::Auth).to receive(:find_with_user_password).with('user', 'password').and_return(user) }
subject! { get '/jwt/auth', parameters, headers }
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 3ea252ed44f..67777ad48bc 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -5,25 +5,33 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
let(:current_user) { nil }
let(:current_params) { {} }
let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
- let(:registry_settings) do
- {
- enabled: true,
- issuer: 'rspec',
- key: nil
- }
- end
let(:payload) { JWT.decode(subject[:token], rsa_key).first }
subject { described_class.new(current_project, current_user, current_params).execute }
before do
- allow(Gitlab.config.registry).to receive_messages(registry_settings)
+ allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key)
end
- shared_examples 'an authenticated' do
+ shared_examples 'a valid token' do
it { is_expected.to include(:token) }
it { expect(payload).to include('access') }
+
+ context 'a expirable' do
+ let(:expires_at) { Time.at(payload['exp']) }
+ let(:expire_delay) { 10 }
+
+ context 'for default configuration' do
+ it { expect(expires_at).not_to be_within(2.seconds).of(Time.now + expire_delay.minutes) }
+ end
+
+ context 'for changed configuration' do
+ before { stub_application_setting(container_registry_token_expire_delay: expire_delay) }
+
+ it { expect(expires_at).to be_within(2.seconds).of(Time.now + expire_delay.minutes) }
+ end
+ end
end
shared_examples 'a accessible' do
@@ -35,10 +43,15 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
}]
end
- it_behaves_like 'an authenticated'
+ it_behaves_like 'a valid token'
it { expect(payload).to include('access' => access) }
end
+ shared_examples 'an inaccessible' do
+ it_behaves_like 'a valid token'
+ it { expect(payload).to include('access' => []) }
+ end
+
shared_examples 'a pullable' do
it_behaves_like 'a accessible' do
let(:actions) { ['pull'] }
@@ -59,19 +72,26 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
shared_examples 'a forbidden' do
it { is_expected.to include(http_status: 403) }
- it { is_expected.to_not include(:token) }
+ it { is_expected.not_to include(:token) }
+ end
+
+ describe '#full_access_token' do
+ let(:project) { create(:empty_project) }
+ let(:token) { described_class.full_access_token(project.path_with_namespace) }
+
+ subject { { token: token } }
+
+ it_behaves_like 'a accessible' do
+ let(:actions) { ['*'] }
+ end
end
context 'user authorization' do
let(:project) { create(:project) }
let(:current_user) { create(:user) }
- context 'allow to use offline_token' do
- let(:current_params) do
- { offline_token: true }
- end
-
- it_behaves_like 'an authenticated'
+ context 'allow to use scope-less authentication' do
+ it_behaves_like 'a valid token'
end
context 'allow developer to push images' do
@@ -111,19 +131,15 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
{ scope: "repository:#{project.path_with_namespace}:pull,push" }
end
- it_behaves_like 'a forbidden'
+ it_behaves_like 'an inaccessible'
end
end
context 'project authorization' do
let(:current_project) { create(:empty_project) }
- context 'disallow to use offline_token' do
- let(:current_params) do
- { offline_token: true }
- end
-
- it_behaves_like 'a forbidden'
+ context 'allow to use scope-less authentication' do
+ it_behaves_like 'a valid token'
end
context 'allow to pull and push images' do
@@ -149,7 +165,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'disallow for private' do
let(:project) { create(:empty_project, :private) }
- it_behaves_like 'a forbidden'
+ it_behaves_like 'an inaccessible'
end
end
@@ -160,18 +176,28 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
context 'disallow for all' do
let(:project) { create(:empty_project, :public) }
- it_behaves_like 'a forbidden'
+ it_behaves_like 'an inaccessible'
end
end
end
- end
- context 'unauthorized' do
- context 'disallow to use offline_token' do
- let(:current_params) do
- { offline_token: true }
+ context 'for project without container registry' do
+ let(:project) { create(:empty_project, :public, container_registry_enabled: false) }
+
+ before { project.update(container_registry_enabled: false) }
+
+ context 'disallow when pulling' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
+
+ it_behaves_like 'an inaccessible'
end
+ end
+ end
+ context 'unauthorized' do
+ context 'disallow to use scope-less authentication' do
it_behaves_like 'a forbidden'
end
diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb
index ecc3a88a262..984b78487d4 100644
--- a/spec/services/ci/create_builds_service_spec.rb
+++ b/spec/services/ci/create_builds_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::CreateBuildsService, services: true do
- let(:commit) { create(:ci_commit, ref: 'master') }
+ let(:pipeline) { create(:ci_pipeline, ref: 'master') }
let(:user) { create(:user) }
describe '#execute' do
@@ -9,7 +9,7 @@ describe Ci::CreateBuildsService, services: true do
#
subject do
- described_class.new(commit).execute(commit, nil, user, status)
+ described_class.new(pipeline).execute('test', nil, user, status)
end
context 'next builds available' do
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index dbdc5370bd8..ae4b7aca820 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -6,7 +6,7 @@ describe Ci::CreateTriggerRequestService, services: true do
let(:trigger) { create(:ci_trigger, project: project) }
before do
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
describe :execute do
@@ -27,8 +27,8 @@ describe Ci::CreateTriggerRequestService, services: true do
subject { service.execute(project, trigger, 'master') }
before do
- stub_ci_commit_yaml_file('{}')
- FactoryGirl.create :ci_commit, project: project
+ stub_ci_pipeline_yaml_file('{}')
+ FactoryGirl.create :ci_pipeline, project: project
end
it { expect(subject).to be_nil }
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index 4cc4b3870d1..476a888e394 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -5,8 +5,8 @@ module Ci
let(:service) { ImageForBuildService.new }
let(:project) { FactoryGirl.create(:empty_project) }
let(:commit_sha) { '01234567890123456789' }
- let(:commit) { project.ensure_ci_commit(commit_sha, 'master') }
- let(:build) { FactoryGirl.create(:ci_build, commit: commit) }
+ let(:commit) { project.ensure_pipeline(commit_sha, 'master') }
+ let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) }
describe :execute do
before { build }
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index e81f9e757ac..d91fc574299 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -4,8 +4,8 @@ module Ci
describe RegisterBuildService, services: true do
let!(:service) { RegisterBuildService.new }
let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false }
- let!(:commit) { FactoryGirl.create :ci_commit, project: project }
- let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit }
+ let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+ let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline }
let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) }
let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) }
diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb
index ea5dcfa068a..a5b4d9f05de 100644
--- a/spec/services/create_commit_builds_service_spec.rb
+++ b/spec/services/create_commit_builds_service_spec.rb
@@ -6,12 +6,12 @@ describe CreateCommitBuildsService, services: true do
let(:user) { nil }
before do
- stub_ci_commit_to_return_yaml_file
+ stub_ci_pipeline_to_return_yaml_file
end
describe :execute do
context 'valid params' do
- let(:commit) do
+ let(:pipeline) do
service.execute(project, user,
ref: 'refs/heads/master',
before: '00000000',
@@ -20,11 +20,11 @@ describe CreateCommitBuildsService, services: true do
)
end
- it { expect(commit).to be_kind_of(Ci::Commit) }
- it { expect(commit).to be_valid }
- it { expect(commit).to be_persisted }
- it { expect(commit).to eq(project.ci_commits.last) }
- it { expect(commit.builds.first).to be_kind_of(Ci::Build) }
+ it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
+ it { expect(pipeline).to be_valid }
+ it { expect(pipeline).to be_persisted }
+ it { expect(pipeline).to eq(project.pipelines.last) }
+ it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
end
context "skip tag if there is no build for it" do
@@ -40,7 +40,7 @@ describe CreateCommitBuildsService, services: true do
it "creates commit if there is no appropriate job but deploy job has right ref setting" do
config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } })
- stub_ci_commit_yaml_file(config)
+ stub_ci_pipeline_yaml_file(config)
result = service.execute(project, user,
ref: 'refs/heads/0_1',
@@ -52,8 +52,8 @@ describe CreateCommitBuildsService, services: true do
end
end
- it 'skips creating ci_commit for refs without .gitlab-ci.yml' do
- stub_ci_commit_yaml_file(nil)
+ it 'skips creating pipeline for refs without .gitlab-ci.yml' do
+ stub_ci_pipeline_yaml_file(nil)
result = service.execute(project, user,
ref: 'refs/heads/0_1',
before: '00000000',
@@ -61,115 +61,115 @@ describe CreateCommitBuildsService, services: true do
commits: [{ message: 'Message' }]
)
expect(result).to be_falsey
- expect(Ci::Commit.count).to eq(0)
+ expect(Ci::Pipeline.count).to eq(0)
end
it 'fails commits if yaml is invalid' do
message = 'message'
- allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message }
- stub_ci_commit_yaml_file('invalid: file: file')
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+ stub_ci_pipeline_yaml_file('invalid: file: file')
commits = [{ message: message }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit).to be_persisted
- expect(commit.builds.any?).to be false
- expect(commit.status).to eq('failed')
- expect(commit.yaml_errors).to_not be_nil
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq('failed')
+ expect(pipeline.yaml_errors).not_to be_nil
end
describe :ci_skip? do
let(:message) { "some message[ci skip]" }
before do
- allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message }
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
end
it "skips builds creation if there is [ci skip] tag in commit message" do
commits = [{ message: message }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit).to be_persisted
- expect(commit.builds.any?).to be false
- expect(commit.status).to eq("skipped")
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
end
it "does not skips builds creation if there is no [ci skip] tag in commit message" do
- allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { "some message" }
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
commits = [{ message: "some message" }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
-
- expect(commit).to be_persisted
- expect(commit.builds.first.name).to eq("staging")
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.first.name).to eq("staging")
end
it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
- stub_ci_commit_yaml_file('invalid: file: fiile')
+ stub_ci_pipeline_yaml_file('invalid: file: fiile')
commits = [{ message: message }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit).to be_persisted
- expect(commit.builds.any?).to be false
- expect(commit.status).to eq("skipped")
- expect(commit.yaml_errors).to be_nil
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ expect(pipeline.yaml_errors).to be_nil
end
end
it "skips build creation if there are already builds" do
- allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { gitlab_ci_yaml }
+ allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml }
commits = [{ message: "message" }]
- commit = service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit).to be_persisted
- expect(commit.builds.count(:all)).to eq(2)
+ pipeline = service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.count(:all)).to eq(2)
- commit = service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit).to be_persisted
- expect(commit.builds.count(:all)).to eq(2)
+ pipeline = service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.count(:all)).to eq(2)
end
it "creates commit with failed status if yaml is invalid" do
- stub_ci_commit_yaml_file('invalid: file')
+ stub_ci_pipeline_yaml_file('invalid: file')
commits = [{ message: "some message" }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
+ pipeline = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
- expect(commit).to be_persisted
- expect(commit.status).to eq("failed")
- expect(commit.builds.any?).to be false
+ expect(pipeline).to be_persisted
+ expect(pipeline.status).to eq("failed")
+ expect(pipeline.builds.any?).to be false
end
end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index eeab540c2fd..18692f1279a 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -158,49 +158,6 @@ describe GitPushService, services: true do
end
end
- describe "Updates main language" do
- context "before push" do
- it { expect(project.main_language).to eq(nil) }
- end
-
- context "after push" do
- def execute
- execute_service(project, user, @oldrev, @newrev, ref)
- end
-
- context "to master" do
- let(:ref) { @ref }
-
- context 'when main_language is nil' do
- it 'obtains the language from the repository' do
- expect(project.repository).to receive(:main_language)
- execute
- end
-
- it 'sets the project main language' do
- execute
- expect(project.main_language).to eq("Ruby")
- end
- end
-
- context 'when main_language is already set' do
- it 'does not check the repository' do
- execute # do an initial run to simulate lang being preset
- expect(project.repository).not_to receive(:main_language)
- execute
- end
- end
- end
-
- context "to other branch" do
- let(:ref) { 'refs/heads/feature/branch' }
-
- it { expect(project.main_language).to eq(nil) }
- end
- end
- end
-
-
describe "Updates git attributes" do
context "for default branch" do
it "calls the copy attributes method for the first push to the default branch" do
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 6aefb48a4e8..71a0b8e2a12 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -13,8 +13,8 @@ describe Groups::CreateService, services: true do
end
context "cannot create group with restricted visibility level" do
- before { allow(current_application_settings).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) }
- it { is_expected.to_not be_persisted }
+ before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) }
+ it { is_expected.not_to be_persisted }
end
end
end
diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb
index e91906d0d49..4a689e64dc5 100644
--- a/spec/services/issues/bulk_update_service_spec.rb
+++ b/spec/services/issues/bulk_update_service_spec.rb
@@ -1,119 +1,265 @@
require 'spec_helper'
describe Issues::BulkUpdateService, services: true do
- let(:issue) { create(:issue, project: @project) }
-
- before do
- @user = create :user
- opts = {
- name: "GitLab",
- namespace: @user.namespace
- }
- @project = Projects::CreateService.new(@user, opts).execute
- end
+ let(:user) { create(:user) }
+ let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute }
- describe :close_issue do
+ let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute }
- before do
- @issues = 5.times.collect do
- create(:issue, project: @project)
- end
- @params = {
+ describe :close_issue do
+ let(:issues) { create_list(:issue, 5, project: project) }
+ let(:params) do
+ {
state_event: 'close',
- issues_ids: @issues.map(&:id)
+ issues_ids: issues.map(&:id).join(',')
}
end
- it do
- result = Issues::BulkUpdateService.new(@project, @user, @params).execute
+ it 'succeeds and returns the correct number of issues updated' do
expect(result[:success]).to be_truthy
- expect(result[:count]).to eq(@issues.count)
-
- expect(@project.issues.opened).to be_empty
- expect(@project.issues.closed).not_to be_empty
+ expect(result[:count]).to eq(issues.count)
end
+ it 'closes all the issues passed' do
+ expect(project.issues.opened).to be_empty
+ expect(project.issues.closed).not_to be_empty
+ end
end
describe :reopen_issues do
-
- before do
- @issues = 5.times.collect do
- create(:closed_issue, project: @project)
- end
- @params = {
+ let(:issues) { create_list(:closed_issue, 5, project: project) }
+ let(:params) do
+ {
state_event: 'reopen',
- issues_ids: @issues.map(&:id)
+ issues_ids: issues.map(&:id).join(',')
}
end
- it do
- result = Issues::BulkUpdateService.new(@project, @user, @params).execute
+ it 'succeeds and returns the correct number of issues updated' do
expect(result[:success]).to be_truthy
- expect(result[:count]).to eq(@issues.count)
-
- expect(@project.issues.closed).to be_empty
- expect(@project.issues.opened).not_to be_empty
+ expect(result[:count]).to eq(issues.count)
end
+ it 'reopens all the issues passed' do
+ expect(project.issues.closed).to be_empty
+ expect(project.issues.opened).not_to be_empty
+ end
end
- describe :update_assignee do
+ describe 'updating assignee' do
+ let(:issue) do
+ create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) }
+ end
- before do
- @new_assignee = create :user
- @params = {
- issues_ids: [issue.id],
- assignee_id: @new_assignee.id
+ let(:params) do
+ {
+ assignee_id: assignee_id,
+ issues_ids: issue.id.to_s
}
end
- it do
- result = Issues::BulkUpdateService.new(@project, @user, @params).execute
- expect(result[:success]).to be_truthy
- expect(result[:count]).to eq(1)
+ context 'when the new assignee ID is a valid user' do
+ let(:new_assignee) { create(:user) }
+ let(:assignee_id) { new_assignee.id }
- expect(@project.issues.first.assignee).to eq(@new_assignee)
- end
+ it 'succeeds' do
+ expect(result[:success]).to be_truthy
+ expect(result[:count]).to eq(1)
+ end
- it 'allows mass-unassigning' do
- @project.issues.first.update_attribute(:assignee, @new_assignee)
- expect(@project.issues.first.assignee).not_to be_nil
+ it 'updates the assignee to the use ID passed' do
+ expect(issue.reload.assignee).to eq(new_assignee)
+ end
+ end
- @params[:assignee_id] = -1
+ context 'when the new assignee ID is -1' do
+ let(:assignee_id) { -1 }
- Issues::BulkUpdateService.new(@project, @user, @params).execute
- expect(@project.issues.first.assignee).to be_nil
+ it 'unassigns the issues' do
+ expect(issue.reload.assignee).to be_nil
+ end
end
- it 'does not unassign when assignee_id is not present' do
- @project.issues.first.update_attribute(:assignee, @new_assignee)
- expect(@project.issues.first.assignee).not_to be_nil
-
- @params[:assignee_id] = ''
+ context 'when the new assignee ID is not present' do
+ let(:assignee_id) { nil }
- Issues::BulkUpdateService.new(@project, @user, @params).execute
- expect(@project.issues.first.assignee).not_to be_nil
+ it 'does not unassign' do
+ expect(issue.reload.assignee).to eq(user)
+ end
end
end
- describe :update_milestone do
+ describe 'updating milestones' do
+ let(:issue) { create(:issue, project: project) }
+ let(:milestone) { create(:milestone, project: project) }
- before do
- @milestone = create(:milestone, project: @project)
- @params = {
- issues_ids: [issue.id],
- milestone_id: @milestone.id
+ let(:params) do
+ {
+ issues_ids: issue.id.to_s,
+ milestone_id: milestone.id
}
end
- it do
- result = Issues::BulkUpdateService.new(@project, @user, @params).execute
+ it 'succeeds' do
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
+ end
- expect(@project.issues.first.milestone).to eq(@milestone)
+ it 'updates the issue milestone' do
+ expect(project.issues.first.milestone).to eq(milestone)
end
end
+ describe 'updating labels' do
+ def create_issue_with_labels(labels)
+ create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) }
+ end
+
+ let(:bug) { create(:label, project: project) }
+ let(:regression) { create(:label, project: project) }
+ let(:merge_requests) { create(:label, project: project) }
+
+ let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) }
+ let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) }
+ let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) }
+ let(:issue_no_labels) { create(:issue, project: project) }
+ let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] }
+
+ let(:labels) { [] }
+ let(:add_labels) { [] }
+ let(:remove_labels) { [] }
+
+ let(:params) do
+ {
+ label_ids: labels.map(&:id),
+ add_label_ids: add_labels.map(&:id),
+ remove_label_ids: remove_labels.map(&:id),
+ issues_ids: issues.map(&:id).join(',')
+ }
+ end
+
+ context 'when label_ids are passed' do
+ let(:issues) { [issue_all_labels, issue_no_labels] }
+ let(:labels) { [bug, regression] }
+
+ it 'updates the labels of all issues passed to the labels passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id)))
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ end
+
+ context 'when those label IDs are empty' do
+ let(:labels) { [] }
+
+ it 'updates the issues passed to have no labels' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty)
+ end
+ end
+ end
+
+ context 'when add_label_ids are passed' do
+ let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
+ let(:add_labels) { [bug, regression, merge_requests] }
+
+ it 'adds those label IDs to all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id)))
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ end
+ end
+
+ context 'when remove_label_ids are passed' do
+ let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
+ let(:remove_labels) { [bug, regression, merge_requests] }
+
+ it 'removes those label IDs from all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty)
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ end
+ end
+
+ context 'when add_label_ids and remove_label_ids are passed' do
+ let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] }
+ let(:add_labels) { [bug] }
+ let(:remove_labels) { [merge_requests] }
+
+ it 'adds the label IDs to all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
+ end
+
+ it 'removes the label IDs from all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ end
+ end
+
+ context 'when add_label_ids and label_ids are passed' do
+ let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] }
+ let(:labels) { [merge_requests] }
+ let(:add_labels) { [regression] }
+
+ it 'adds the label IDs to all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id))
+ end
+
+ it 'ignores the label IDs parameter' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_no_labels.label_ids).to be_empty
+ end
+ end
+
+ context 'when remove_label_ids and label_ids are passed' do
+ let(:issues) { [issue_no_labels, issue_bug_and_regression] }
+ let(:labels) { [merge_requests] }
+ let(:remove_labels) { [regression] }
+
+ it 'remove the label IDs from all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
+ end
+
+ it 'ignores the label IDs parameter' do
+ expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id)
+ end
+ end
+
+ context 'when add_label_ids, remove_label_ids, and label_ids are passed' do
+ let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] }
+ let(:labels) { [regression] }
+ let(:add_labels) { [bug] }
+ let(:remove_labels) { [merge_requests] }
+
+ it 'adds the label IDs to all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id))
+ end
+
+ it 'removes the label IDs from all issues passed' do
+ expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id)
+ end
+
+ it 'ignores the label IDs parameter' do
+ expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
+ end
+
+ it 'does not update issues not passed in' do
+ expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id)
+ end
+ end
+ end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index ac28b6f71f9..1ee9f3aae4d 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -54,8 +54,8 @@ describe Issues::CreateService, services: true do
label_ids: [label.id] }
end
- it 'does not assign label'do
- expect(issue.labels).to_not include label
+ it 'does not assign label' do
+ expect(issue.labels).not_to include label
end
end
@@ -69,7 +69,7 @@ describe Issues::CreateService, services: true do
end
it 'does not assign milestone' do
- expect(issue.milestone).to_not eq milestone
+ expect(issue.milestone).not_to eq milestone
end
end
end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index c15e26189a5..93bf0f64963 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -39,6 +39,7 @@ describe Issues::MoveService, services: true do
let!(:milestone2) do
create(:milestone, project_id: new_project.id, title: 'v9.0')
end
+ let!(:award_emoji) { create(:award_emoji, awardable: old_issue) }
let!(:new_issue) { move_service.execute(old_issue, new_project) }
end
@@ -115,6 +116,10 @@ describe Issues::MoveService, services: true do
it 'preserves create time' do
expect(old_issue.created_at).to eq new_issue.created_at
end
+
+ it 'moves the award emoji' do
+ expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name
+ end
end
context 'issue with notes' do
@@ -194,10 +199,10 @@ describe Issues::MoveService, services: true do
include_context 'issue move executed'
it 'rewrites uploads in description' do
- expect(new_issue.description).to_not eq description
+ expect(new_issue.description).not_to eq description
expect(new_issue.description)
.to match(/Text and #{FileUploader::MARKDOWN_PATTERN}/)
- expect(new_issue.description).to_not include uploader.secret
+ expect(new_issue.description).not_to include uploader.secret
end
end
end
@@ -231,7 +236,7 @@ describe Issues::MoveService, services: true do
context 'user is reporter in both projects' do
include_context 'user can move issue'
- it { expect { move }.to_not raise_error }
+ it { expect { move }.not_to raise_error }
end
context 'user is reporter only in new project' do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 52f69306994..dacbcd8fb46 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
require 'spec_helper'
describe Issues::UpdateService, services: true do
@@ -27,11 +28,6 @@ describe Issues::UpdateService, services: true do
end
end
- def update_issue(opts)
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
- @issue.reload
- end
-
context "valid params" do
before do
opts = {
@@ -39,7 +35,8 @@ describe Issues::UpdateService, services: true do
description: 'Also please fix',
assignee_id: user2.id,
state_event: 'close',
- label_ids: [label.id]
+ label_ids: [label.id],
+ confidential: true
}
perform_enqueued_jobs do
@@ -79,13 +76,25 @@ describe Issues::UpdateService, services: true do
end
it 'creates system note about title change' do
- note = find_note('Title changed')
+ note = find_note('Changed title:')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
+ end
+
+ it 'creates system note about confidentiality change' do
+ note = find_note('Made the issue confidential')
expect(note).not_to be_nil
- expect(note.note).to eq 'Title changed from **Old title** to **New title**'
+ expect(note.note).to eq 'Made the issue confidential'
end
end
+ def update_issue(opts)
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ @issue.reload
+ end
+
context 'todos' do
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
@@ -265,5 +274,50 @@ describe Issues::UpdateService, services: true do
end
end
end
+
+ context 'updating labels' do
+ let(:label3) { create(:label, project: project) }
+ let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload }
+
+ context 'when add_label_ids and label_ids are passed' do
+ let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } }
+
+ it 'ignores the label_ids parameter' do
+ expect(result.label_ids).not_to include(label.id)
+ end
+
+ it 'adds the passed labels' do
+ expect(result.label_ids).to include(label3.id)
+ end
+ end
+
+ context 'when remove_label_ids and label_ids are passed' do
+ let(:params) { { label_ids: [], remove_label_ids: [label.id] } }
+
+ before { issue.update_attributes(labels: [label, label3]) }
+
+ it 'ignores the label_ids parameter' do
+ expect(result.label_ids).not_to be_empty
+ end
+
+ it 'removes the passed labels' do
+ expect(result.label_ids).not_to include(label.id)
+ end
+ end
+
+ context 'when add_label_ids and remove_label_ids are passed' do
+ let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } }
+
+ before { issue.update_attributes(labels: [label]) }
+
+ it 'adds the passed labels' do
+ expect(result.label_ids).to include(label3.id)
+ end
+
+ it 'removes the passed labels' do
+ expect(result.label_ids).not_to include(label.id)
+ end
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
new file mode 100644
index 00000000000..dd656c3bbb7
--- /dev/null
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+
+# Write specs in this file.
+describe MergeRequests::AddTodoWhenBuildFailsService do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { create(:project) }
+ let(:sha) { '1234567890abcdef1234567890abcdef12345678' }
+ let(:pipeline) { create(:ci_pipeline_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) }
+ let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user, commit_message: 'Awesome message') }
+ let(:todo_service) { TodoService.new }
+
+ let(:merge_request) do
+ create(:merge_request, merge_user: user, source_branch: 'master',
+ target_branch: 'feature', source_project: project, target_project: project,
+ state: 'opened')
+ end
+
+ before do
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow(service).to receive(:todo_service).and_return(todo_service)
+ end
+
+ describe '#execute' do
+ context 'commit status with ref' do
+ let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: pipeline) }
+
+ it 'notifies the todo service' do
+ expect(todo_service).to receive(:merge_request_build_failed).with(merge_request)
+ service.execute(commit_status)
+ end
+ end
+
+ context 'commit status with non-HEAD ref' do
+ let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) }
+
+ it 'does not notify the todo service' do
+ expect(todo_service).not_to receive(:merge_request_build_failed)
+ service.execute(commit_status)
+ end
+ end
+
+ context 'commit status without ref' do
+ let(:commit_status) { create(:generic_commit_status) }
+
+ it 'does not notify the todo service' do
+ expect(todo_service).not_to receive(:merge_request_build_failed)
+ service.execute(commit_status)
+ end
+ end
+ end
+
+ describe '#close' do
+ context 'commit status with ref' do
+ let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: pipeline) }
+
+ it 'notifies the todo service' do
+ expect(todo_service).to receive(:merge_request_build_retried).with(merge_request)
+ service.close(commit_status)
+ end
+ end
+
+ context 'commit status with non-HEAD ref' do
+ let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) }
+
+ it 'does not notify the todo service' do
+ expect(todo_service).not_to receive(:merge_request_build_retried)
+ service.close(commit_status)
+ end
+ end
+
+ context 'commit status without ref' do
+ let(:commit_status) { create(:generic_commit_status) }
+
+ it 'does not notify the todo service' do
+ expect(todo_service).not_to receive(:merge_request_build_retried)
+ service.close(commit_status)
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 120f4d6a669..e433f49872d 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -12,7 +12,8 @@ describe MergeRequests::CreateService, services: true do
title: 'Awesome merge_request',
description: 'please fix',
source_branch: 'feature',
- target_branch: 'master'
+ target_branch: 'master',
+ force_remove_source_branch: '1'
}
end
@@ -29,6 +30,7 @@ describe MergeRequests::CreateService, services: true do
it { expect(@merge_request).to be_valid }
it { expect(@merge_request.title).to eq('Awesome merge_request') }
it { expect(@merge_request.assignee).to be_nil }
+ it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
it 'should execute hooks with default action' do
expect(service).to have_received(:execute_hooks).with(@merge_request)
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index ceb3f97280e..1b0396eb686 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -38,6 +38,21 @@ describe MergeRequests::MergeService, services: true do
end
end
+ context 'remove source branch by author' do
+ let(:service) do
+ merge_request.merge_params['force_remove_source_branch'] = '1'
+ merge_request.save!
+ MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message')
+ end
+
+ it 'removes the source branch' do
+ expect(DeleteBranchService).to receive(:new).
+ with(merge_request.source_project, merge_request.author).
+ and_call_original
+ service.execute(merge_request)
+ end
+ end
+
context "error handling" do
let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
index 52a302e0e1a..4da8146e3d6 100644
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe MergeRequests::MergeWhenBuildSucceedsService do
- let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request) }
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
let(:mr_merge_if_green_enabled) do
create(:merge_request, merge_when_build_succeeds: true, merge_user: user,
@@ -10,14 +10,18 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
source_project: project, target_project: project, state: "opened")
end
- let(:project) { create(:project) }
- let(:ci_commit) { create(:ci_commit_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) }
+ let(:pipeline) { create(:ci_pipeline_with_one_job, ref: mr_merge_if_green_enabled.source_branch, project: project) }
let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') }
describe "#execute" do
+ let(:merge_request) do
+ create(:merge_request, target_project: project, source_project: project,
+ source_branch: "feature", target_branch: 'master')
+ end
+
context 'first time enabling' do
before do
- allow(merge_request).to receive(:ci_commit).and_return(ci_commit)
+ allow(merge_request).to receive(:pipeline).and_return(pipeline)
service.execute(merge_request)
end
@@ -39,9 +43,9 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
before do
- allow(mr_merge_if_green_enabled).to receive(:ci_commit).and_return(ci_commit)
+ allow(mr_merge_if_green_enabled).to receive(:pipeline).and_return(pipeline)
allow(mr_merge_if_green_enabled).to receive(:mergeable?).and_return(true)
- allow(ci_commit).to receive(:success?).and_return(true)
+ allow(pipeline).to receive(:success?).and_return(true)
end
it 'updates the merge params' do
@@ -58,8 +62,8 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
it "merges all merge requests with merge when build succeeds enabled" do
- allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
- allow(ci_commit).to receive(:success?).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:success?).and_return(true)
expect(MergeWorker).to receive(:perform_async)
service.trigger(build)
@@ -71,11 +75,11 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") }
it "merges all merge requests with merge when build succeeds enabled" do
- allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
- allow(ci_commit).to receive(:success?).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:success?).and_return(true)
allow(old_build).to receive(:sha).and_return('1234abcdef')
- expect(MergeWorker).to_not receive(:perform_async)
+ expect(MergeWorker).not_to receive(:perform_async)
service.trigger(old_build)
end
end
@@ -88,16 +92,16 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
it "doesn't merge a requests for status on other branch" do
allow(project.repository).to receive(:branch_names_contains).with(commit_status.sha).and_return([])
- expect(MergeWorker).to_not receive(:perform_async)
+ expect(MergeWorker).not_to receive(:perform_async)
service.trigger(commit_status)
end
it 'discovers branches and merges all merge requests when status is success' do
allow(project.repository).to receive(:branch_names_contains).
with(commit_status.sha).and_return([mr_merge_if_green_enabled.source_branch])
- allow(ci_commit).to receive(:success?).and_return(true)
- allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit)
- allow(ci_commit).to receive(:success?).and_return(true)
+ allow(pipeline).to receive(:success?).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:success?).and_return(true)
expect(MergeWorker).to receive(:perform_async)
service.trigger(commit_status)
@@ -106,23 +110,23 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
context 'properly handles multiple stages' do
let(:ref) { mr_merge_if_green_enabled.source_branch }
- let(:build) { create(:ci_build, commit: ci_commit, ref: ref, name: 'build', stage: 'build') }
- let(:test) { create(:ci_build, commit: ci_commit, ref: ref, name: 'test', stage: 'test') }
+ let(:build) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
+ let(:test) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') }
before do
# This behavior of MergeRequest: we instantiate a new object
- allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_wrap_original do
- Ci::Commit.find(ci_commit.id)
+ allow_any_instance_of(MergeRequest).to receive(:pipeline).and_wrap_original do
+ Ci::Pipeline.find(pipeline.id)
end
# We create test after the build
- allow(ci_commit).to receive(:create_next_builds).and_wrap_original do
+ allow(pipeline).to receive(:create_next_builds).and_wrap_original do
test
end
end
it "doesn't merge if some stages failed" do
- expect(MergeWorker).to_not receive(:perform_async)
+ expect(MergeWorker).not_to receive(:perform_async)
build.success
test.drop
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index fea8182bd30..31b93850c7c 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -27,6 +27,20 @@ describe MergeRequests::RefreshService, services: true do
target_branch: 'feature',
target_project: @project)
+ @build_failed_todo = create(:todo,
+ :build_failed,
+ user: @user,
+ project: @project,
+ target: @merge_request,
+ author: @user)
+
+ @fork_build_failed_todo = create(:todo,
+ :build_failed,
+ user: @user,
+ project: @project,
+ target: @merge_request,
+ author: @user)
+
@commits = @merge_request.commits
@oldrev = @commits.last.id
@@ -51,6 +65,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.merge_when_build_succeeds).to be_falsey}
it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
+ it { expect(@build_failed_todo).to be_done }
+ it { expect(@fork_build_failed_todo).to be_done }
end
context 'push to origin repo target branch' do
@@ -63,6 +79,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'manual merge of source branch' do
@@ -82,6 +100,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.diffs.size).to be > 0 }
it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to fork repo source branch' do
@@ -101,6 +121,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_open }
it { expect(@fork_merge_request.notes.last.note).to include('Added 4 commits') }
it { expect(@fork_merge_request).to be_open }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to fork repo target branch' do
@@ -113,6 +135,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@fork_merge_request).to be_open }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push to origin repo target branch after fork project was removed' do
@@ -126,6 +150,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
+ it { expect(@build_failed_todo).to be_pending }
+ it { expect(@fork_build_failed_todo).to be_pending }
end
context 'push new branch that exists in a merge request' do
@@ -153,6 +179,8 @@ describe MergeRequests::RefreshService, services: true do
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
+ @build_failed_todo.reload
+ @fork_build_failed_todo.reload
end
end
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 213e8c2eb3a..d4ebe28c276 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -39,7 +39,8 @@ describe MergeRequests::UpdateService, services: true do
assignee_id: user2.id,
state_event: 'close',
label_ids: [label.id],
- target_branch: 'target'
+ target_branch: 'target',
+ force_remove_source_branch: '1'
}
end
@@ -61,6 +62,7 @@ describe MergeRequests::UpdateService, services: true do
it { expect(@merge_request.labels.count).to eq(1) }
it { expect(@merge_request.labels.first.title).to eq(label.name) }
it { expect(@merge_request.target_branch).to eq('target') }
+ it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
it 'should execute hooks with update action' do
expect(service).to have_received(:execute_hooks).
@@ -90,10 +92,10 @@ describe MergeRequests::UpdateService, services: true do
end
it 'creates system note about title change' do
- note = find_note('Title changed')
+ note = find_note('Changed title:')
expect(note).not_to be_nil
- expect(note.note).to eq 'Title changed from **Old title** to **New title**'
+ expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
end
it 'creates system note about branch change' do
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index ff23f13e1cb..35f576874b8 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -14,7 +14,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
-
+
@note = Notes::CreateService.new(project, user, opts).execute
end
@@ -28,18 +28,16 @@ describe Notes::CreateService, services: true do
project.team << [user, :master]
end
- it "creates emoji note" do
+ it "creates an award emoji" do
opts = {
note: ':smile: ',
noteable_type: 'Issue',
noteable_id: issue.id
}
+ note = Notes::CreateService.new(project, user, opts).execute
- @note = Notes::CreateService.new(project, user, opts).execute
-
- expect(@note).to be_valid
- expect(@note.note).to eq('smile')
- expect(@note.is_award).to be_truthy
+ expect(note).to be_valid
+ expect(note.name).to eq('smile')
end
it "creates regular note if emoji name is invalid" do
@@ -48,12 +46,22 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
+ note = Notes::CreateService.new(project, user, opts).execute
+
+ expect(note).to be_valid
+ expect(note.note).to eq(opts[:note])
+ end
+
+ it "normalizes the emoji name" do
+ opts = {
+ note: ':+1:',
+ noteable_type: 'Issue',
+ noteable_id: issue.id
+ }
- @note = Notes::CreateService.new(project, user, opts).execute
+ expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
- expect(@note).to be_valid
- expect(@note.note).to eq(opts[:note])
- expect(@note.is_award).to be_falsy
+ Notes::CreateService.new(project, user, opts).execute
end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 4bbc4ddc3ab..e871a103d42 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -66,11 +66,13 @@ describe NotificationService, services: true do
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_email(@subscribed_participant)
+ should_not_email(@u_guest_watcher)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@unsubscriber)
should_not_email(@u_outsider_mentioned)
+ should_not_email(@u_lazy_participant)
end
it 'filters out "mentioned in" notes' do
@@ -79,6 +81,20 @@ describe NotificationService, services: true do
expect(Notify).not_to receive(:note_issue_email)
notification.new_note(mentioned_note)
end
+
+ context 'participating' do
+ context 'by note' do
+ before do
+ ActionMailer::Base.deliveries.clear
+ note.author = @u_lazy_participant
+ note.save
+ notification.new_note(note)
+ end
+
+
+ it { should_not_email(@u_lazy_participant) }
+ end
+ end
end
describe 'new note on issue in project that belongs to a group' do
@@ -100,10 +116,12 @@ describe NotificationService, services: true do
should_email(note.noteable.author)
should_email(note.noteable.assignee)
should_email(@u_mentioned)
+ should_not_email(@u_guest_watcher)
should_not_email(@u_watcher)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
end
end
@@ -114,12 +132,14 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") }
it 'filters out users that can not read the issue' do
project.team << [member, :developer]
+ project.team << [guest, :guest]
expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
@@ -128,6 +148,7 @@ describe NotificationService, services: true do
notification.new_note(note)
should_not_email(non_member)
+ should_not_email(guest)
should_email(author)
should_email(assignee)
should_email(member)
@@ -160,6 +181,7 @@ describe NotificationService, services: true do
should_email(member)
end
+ should_email(@u_guest_watcher)
should_email(note.noteable.author)
should_email(note.noteable.assignee)
should_not_email(note.author)
@@ -201,6 +223,7 @@ describe NotificationService, services: true do
should_email(member)
end
+ should_email(@u_guest_watcher)
should_email(note.noteable.author)
should_not_email(note.author)
should_email(@u_mentioned)
@@ -224,28 +247,32 @@ describe NotificationService, services: true do
it do
notification.new_note(note)
+ should_email(@u_guest_watcher)
should_email(@u_committer)
should_email(@u_watcher)
should_not_email(@u_mentioned)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it do
note.update_attribute(:note, '@mention referenced')
notification.new_note(note)
+ should_email(@u_guest_watcher)
should_email(@u_committer)
should_email(@u_watcher)
should_email(@u_mentioned)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it do
- @u_committer.update_attributes(notification_level: :mention)
+ @u_committer = create_global_setting_for(@u_committer, :mention)
notification.new_note(note)
should_not_email(@u_committer)
end
@@ -269,14 +296,16 @@ describe NotificationService, services: true do
should_email(issue.assignee)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
should_email(@u_participant_mentioned)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it do
- issue.assignee.update_attributes(notification_level: :mention)
+ create_global_setting_for(issue.assignee, :mention)
notification.new_issue(issue, @u_disabled)
should_not_email(issue.assignee)
@@ -296,17 +325,20 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
it "emails subscribers of the issue's labels that can read the issue" do
project.team << [member, :developer]
+ project.team << [guest, :guest]
label = create(:label, issues: [confidential_issue])
label.toggle_subscription(non_member)
label.toggle_subscription(author)
label.toggle_subscription(assignee)
label.toggle_subscription(member)
+ label.toggle_subscription(guest)
label.toggle_subscription(admin)
ActionMailer::Base.deliveries.clear
@@ -315,6 +347,7 @@ describe NotificationService, services: true do
should_not_email(non_member)
should_not_email(author)
+ should_not_email(guest)
should_email(assignee)
should_email(member)
should_email(admin)
@@ -328,11 +361,13 @@ describe NotificationService, services: true do
should_email(issue.assignee)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'emails previous assignee even if he has the "on mention" notif level' do
@@ -342,11 +377,13 @@ describe NotificationService, services: true do
should_email(@u_mentioned)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'emails new assignee even if he has the "on mention" notif level' do
@@ -356,11 +393,13 @@ describe NotificationService, services: true do
expect(issue.assignee).to be @u_mentioned
should_email(issue.assignee)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'emails new assignee' do
@@ -370,11 +409,13 @@ describe NotificationService, services: true do
expect(issue.assignee).to be @u_mentioned
should_email(issue.assignee)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it 'does not email new assignee if they are the current user' do
@@ -383,12 +424,42 @@ describe NotificationService, services: true do
expect(issue.assignee).to be @u_mentioned
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_not_email(issue.assignee)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ issue.update_attribute(:assignee, @u_lazy_participant)
+ notification.reassigned_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reassigned_issue(issue, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ issue.author = @u_lazy_participant
+ notification.reassigned_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -411,6 +482,7 @@ describe NotificationService, services: true do
should_not_email(issue.assignee)
should_not_email(issue.author)
should_not_email(@u_watcher)
+ should_not_email(@u_guest_watcher)
should_not_email(@u_participant_mentioned)
should_not_email(@subscriber)
should_not_email(@watcher_and_subscriber)
@@ -425,6 +497,7 @@ describe NotificationService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
let!(:label_1) { create(:label, issues: [confidential_issue]) }
@@ -432,11 +505,13 @@ describe NotificationService, services: true do
it "emails subscribers of the issue's labels that can read the issue" do
project.team << [member, :developer]
+ project.team << [guest, :guest]
label_2.toggle_subscription(non_member)
label_2.toggle_subscription(author)
label_2.toggle_subscription(assignee)
label_2.toggle_subscription(member)
+ label_2.toggle_subscription(guest)
label_2.toggle_subscription(admin)
ActionMailer::Base.deliveries.clear
@@ -444,6 +519,7 @@ describe NotificationService, services: true do
notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
should_not_email(non_member)
+ should_not_email(guest)
should_email(author)
should_email(assignee)
should_email(member)
@@ -459,12 +535,42 @@ describe NotificationService, services: true do
should_email(issue.assignee)
should_email(issue.author)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ issue.update_attribute(:assignee, @u_lazy_participant)
+ notification.close_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.close_issue(issue, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ issue.author = @u_lazy_participant
+ notification.close_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -475,11 +581,41 @@ describe NotificationService, services: true do
should_email(issue.assignee)
should_email(issue.author)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ issue.update_attribute(:assignee, @u_lazy_participant)
+ notification.reopen_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reopen_issue(issue, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ issue.author = @u_lazy_participant
+ notification.reopen_issue(issue, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
end
@@ -502,8 +638,10 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@watcher_and_subscriber)
should_email(@u_participant_mentioned)
+ should_email(@u_guest_watcher)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
end
it "emails subscribers of the merge request's labels" do
@@ -514,6 +652,36 @@ describe NotificationService, services: true do
should_email(subscriber)
end
+
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.new_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.new_merge_request(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.new_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_not_email(@u_lazy_participant) }
+ end
+ end
end
describe '#reassigned_merge_request' do
@@ -525,9 +693,40 @@ describe NotificationService, services: true do
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
+ should_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
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.reassigned_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reassigned_merge_request(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.reassigned_merge_request(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -555,6 +754,7 @@ describe NotificationService, services: true do
should_not_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
+ should_not_email(@u_lazy_participant)
should_not_email(subscriber_to_label)
should_email(subscriber_to_label2)
end
@@ -566,12 +766,43 @@ describe NotificationService, services: true do
should_email(merge_request.assignee)
should_email(@u_watcher)
+ should_email(@u_guest_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.close_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.close_mr(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.close_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -584,9 +815,40 @@ describe NotificationService, services: true do
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
+ should_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
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.merge_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.merge_mr(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.merge_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
@@ -599,9 +861,40 @@ describe NotificationService, services: true do
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
+ should_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
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.reopen_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.reopen_mr(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.reopen_mr(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
end
end
end
@@ -620,20 +913,29 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participating)
+ should_email(@u_lazy_participant)
+ should_not_email(@u_guest_watcher)
should_not_email(@u_disabled)
end
end
end
def build_team(project)
- @u_watcher = create(:user, notification_level: :watch)
- @u_participating = create(:user, notification_level: :participating)
- @u_participant_mentioned = create(:user, username: 'participant', notification_level: :participating)
- @u_disabled = create(:user, notification_level: :disabled)
- @u_mentioned = create(:user, username: 'mention', notification_level: :mention)
- @u_committer = create(:user, username: 'committer')
- @u_not_mentioned = create(:user, username: 'regular', notification_level: :participating)
- @u_outsider_mentioned = create(:user, username: 'outsider')
+ @u_watcher = create_global_setting_for(create(:user), :watch)
+ @u_participating = create_global_setting_for(create(:user), :participating)
+ @u_participant_mentioned = create_global_setting_for(create(:user, username: 'participant'), :participating)
+ @u_disabled = create_global_setting_for(create(:user), :disabled)
+ @u_mentioned = create_global_setting_for(create(:user, username: 'mention'), :mention)
+ @u_committer = create(:user, username: 'committer')
+ @u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating)
+ @u_outsider_mentioned = create(:user, username: 'outsider')
+
+ # User to be participant by default
+ # This user does not contain any record in notification settings table
+ # It should be treated with a :participating notification_level
+ @u_lazy_participant = create(:user, username: 'lazy-participant')
+
+ create_guest_watcher
project.team << [@u_watcher, :master]
project.team << [@u_participating, :master]
@@ -642,13 +944,29 @@ describe NotificationService, services: true do
project.team << [@u_mentioned, :master]
project.team << [@u_committer, :master]
project.team << [@u_not_mentioned, :master]
+ project.team << [@u_lazy_participant, :master]
+ end
+
+ def create_global_setting_for(user, level)
+ setting = user.global_notification_setting
+ setting.level = level
+ setting.save
+
+ user
+ end
+
+ def create_guest_watcher
+ @u_guest_watcher = create(:user, username: 'guest_watching')
+ setting = @u_guest_watcher.notification_settings_for(project)
+ setting.level = :watch
+ setting.save
end
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
- @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: :participating)
- @watcher_and_subscriber = create(:user, notification_level: :watch)
+ @subscribed_participant = create_global_setting_for(create(:user, username: 'subscribed_participant'), :participating)
+ @watcher_and_subscriber = create_global_setting_for(create(:user), :watch)
project.team << [@subscribed_participant, :master]
project.team << [@subscriber, :master]
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 6108c26a78b..0971fec2e9f 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -33,6 +33,18 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 1
end
+ it 'should not list project confidential issues for project members with guest role' do
+ project.team << [member, :guest]
+
+ autocomplete = described_class.new(project, non_member)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 1
+ end
+
it 'should list project confidential issues for author' do
autocomplete = described_class.new(project, author)
issues = autocomplete.issues.map(&:iid)
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index a5cb6f382e4..29341c5e57e 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -28,6 +28,29 @@ describe Projects::DestroyService, services: true do
it { expect(Dir.exist?(remove_path)).to be_truthy }
end
+ context 'container registry' do
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags('tag')
+ end
+
+ context 'tags deletion succeeds' do
+ it do
+ expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true)
+
+ destroy_project(project, user, {})
+ end
+ end
+
+ context 'tags deletion fails' do
+ before { expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(false) }
+
+ subject { destroy_project(project, user, {}) }
+
+ it { expect{subject}.to raise_error(Projects::DestroyService::DestroyError) }
+ end
+ end
+
def destroy_project(project, user, params)
Projects::DestroyService.new(project, user, params).execute
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index d1ee60a0aea..31bb7120d84 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -42,6 +42,33 @@ describe Projects::ForkService, services: true do
expect(@to_project.builds_enabled?).to be_truthy
end
end
+
+ context "when project has restricted visibility level" do
+ context "and only one visibility level is restricted" do
+ before do
+ @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+ end
+
+ it "creates fork with highest allowed level" do
+ forked_project = fork_project(@from_project, @to_user)
+
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ context "and all visibility levels are restricted" do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE])
+ end
+
+ it "creates fork with private visibility levels" do
+ forked_project = fork_project(@from_project, @to_user)
+
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+ end
end
describe :fork_to_namespace do
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 7f2dcdab960..068c9a1219c 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -49,7 +49,7 @@ describe Projects::ImportService, services: true do
result = subject.execute
expect(result[:status]).to eq :error
- expect(result[:message]).to eq 'Failed to import the repository'
+ expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository"
end
end
@@ -124,7 +124,7 @@ describe Projects::ImportService, services: true do
}
)
- Gitlab.config.omniauth.providers << provider
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 06017317339..d5aa115a074 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -26,6 +26,17 @@ describe Projects::TransferService, services: true do
it { expect(project.namespace).to eq(user.namespace) }
end
+ context 'disallow transfering of project with tags' do
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags('tag')
+ end
+
+ subject { transfer_project(project, user, group) }
+
+ it { is_expected.to be_falsey }
+ end
+
context 'namespace -> not allowed namespace' do
before do
@result = transfer_project(project, user, group)
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5fbf2ae5247..09f0ee3871d 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -208,8 +208,10 @@ describe SystemNoteService, services: true do
end
describe '.merge_when_build_succeeds' do
- let(:ci_commit) { build :ci_commit_without_jobs }
- let(:noteable) { create :merge_request }
+ let(:pipeline) { build(:ci_pipeline_without_jobs )}
+ let(:noteable) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.last_commit) }
@@ -221,8 +223,9 @@ describe SystemNoteService, services: true do
end
describe '.cancel_merge_when_build_succeeds' do
- let(:ci_commit) { build :ci_commit_without_jobs }
- let(:noteable) { create :merge_request }
+ let(:noteable) do
+ create(:merge_request, source_project: project, target_project: project)
+ end
subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) }
@@ -241,15 +244,19 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
expect(subject.note).
- to eq "Title changed from **Old title** to **#{noteable.title}**"
+ to eq "Changed title: **{-Old title-}** → **{+#{noteable.title}+}**"
end
end
+ end
- context 'when noteable does not respond to `title' do
- let(:noteable) { double('noteable') }
+ describe '.change_issue_confidentiality' do
+ subject { described_class.change_issue_confidentiality(noteable, project, author) }
- it 'returns nil' do
- expect(subject).to be_nil
+ context 'when noteable responds to `confidential`' do
+ it_behaves_like 'a system note'
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'Made the issue visible'
end
end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index a075496ee63..26f09cdbaf9 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -5,20 +5,22 @@ describe TodoService, services: true do
let(:assignee) { create(:user) }
let(:non_member) { create(:user) }
let(:member) { create(:user) }
+ let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:john_doe) { create(:user) }
let(:project) { create(:project) }
- let(:mentions) { [author, assignee, john_doe, member, non_member, admin].map(&:to_reference).join(' ') }
+ let(:mentions) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
before do
+ project.team << [guest, :guest]
project.team << [author, :developer]
project.team << [member, :developer]
project.team << [john_doe, :developer]
end
describe 'Issues' do
- let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) }
+ let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
let(:unassigned_issue) { create(:issue, project: project, assignee: nil) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) }
@@ -41,18 +43,20 @@ describe TodoService, services: true do
service.new_issue(issue, author)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
- it 'does not create todo for non project members when issue is confidential' do
+ it 'does not create todo if user can not see the issue when issue is confidential' do
service.new_issue(confidential_issue, john_doe)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::ASSIGNED)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
@@ -81,6 +85,7 @@ describe TodoService, services: true do
service.update_issue(issue, author)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
@@ -92,15 +97,29 @@ describe TodoService, services: true do
expect { service.update_issue(issue, author) }.not_to change(member.todos, :count)
end
- it 'does not create todo for non project members when issue is confidential' do
+ it 'does not create todo if user can not see the issue when issue is confidential' do
service.update_issue(confidential_issue, john_doe)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
+
+ it 'does not create todo when when tasks are marked as completed' do
+ issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
+
+ service.update_issue(issue, author)
+
+ should_not_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: assignee, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: member, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
+ end
end
describe '#close_issue' do
@@ -156,7 +175,6 @@ describe TodoService, services: true do
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
- let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') }
let(:system_note) { create(:system_note, project: project, noteable: issue) }
it 'mark related pending todos to the noteable for the note author as done' do
@@ -169,13 +187,6 @@ describe TodoService, services: true do
expect(second_todo.reload).to be_done
end
- it 'mark related pending todos to the noteable for the award note author as done' do
- service.new_note(award_note, john_doe)
-
- expect(first_todo.reload).to be_done
- expect(second_todo.reload).to be_done
- end
-
it 'does not mark related pending todos it is a system note' do
service.new_note(system_note, john_doe)
@@ -187,18 +198,20 @@ describe TodoService, services: true do
service.new_note(note, john_doe)
should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
- it 'does not create todo for non project members when leaving a note on a confidential issue' do
+ it 'does not create todo if user can not see the issue when leaving a note on a confidential issue' do
service.new_note(note_on_confidential_issue, john_doe)
should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
end
@@ -215,10 +228,18 @@ describe TodoService, services: true do
should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
end
end
+
+ describe '#mark_todo' do
+ it 'creates a todo from a issue' do
+ service.mark_todo(unassigned_issue, author)
+
+ should_create_todo(user: author, target: unassigned_issue, action: Todo::MARKED)
+ end
+ end
end
describe 'Merge Requests' do
- let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: mentions) }
+ let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) }
describe '#new_merge_request' do
@@ -240,6 +261,7 @@ describe TodoService, services: true do
service.new_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -251,6 +273,7 @@ describe TodoService, services: true do
service.update_merge_request(mr_assigned, author)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
@@ -261,6 +284,19 @@ describe TodoService, services: true do
expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end
+
+ it 'does not create todo when when tasks are marked as completed' do
+ mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
+
+ service.update_merge_request(mr_assigned, author)
+
+ should_not_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: assignee, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
+ end
end
describe '#close_merge_request' do
@@ -305,6 +341,42 @@ describe TodoService, services: true do
expect(second_todo.reload).to be_done
end
end
+
+ describe '#new_award_emoji' do
+ it 'marks related pending todos to the target for the user as done' do
+ todo = create(:todo, user: john_doe, project: project, target: mr_assigned, author: author)
+ service.new_award_emoji(mr_assigned, john_doe)
+
+ expect(todo.reload).to be_done
+ end
+ end
+
+ describe '#merge_request_build_failed' do
+ it 'creates a pending todo for the merge request author' do
+ service.merge_request_build_failed(mr_unassigned)
+
+ should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED)
+ end
+ end
+
+ describe '#merge_request_push' do
+ it 'marks related pending todos to the target for the user as done' do
+ first_todo = create(:todo, :build_failed, user: author, project: project, target: mr_assigned, author: john_doe)
+ second_todo = create(:todo, :build_failed, user: john_doe, project: project, target: mr_assigned, author: john_doe)
+ service.merge_request_push(mr_assigned, author)
+
+ expect(first_todo.reload).to be_done
+ expect(second_todo.reload).not_to be_done
+ end
+ end
+
+ describe '#mark_todo' do
+ it 'creates a todo from a merge request' do
+ service.mark_todo(mr_unassigned, author)
+
+ should_create_todo(user: author, target: mr_unassigned, action: Todo::MARKED)
+ end
+ end
end
def should_create_todo(attributes = {})
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 576d16e7ea3..b43f38ef202 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -16,6 +16,11 @@ require 'shoulda/matchers'
require 'sidekiq/testing/inline'
require 'rspec/retry'
+if ENV['CI']
+ require 'knapsack'
+ Knapsack::Adapters::RSpecAdapter.bind
+end
+
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb
new file mode 100644
index 00000000000..553fe9f1fbc
--- /dev/null
+++ b/spec/support/fake_u2f_device.rb
@@ -0,0 +1,36 @@
+class FakeU2fDevice
+ def initialize(page)
+ @page = page
+ end
+
+ def respond_to_u2f_registration
+ app_id = @page.evaluate_script('gon.u2f.app_id')
+ challenges = @page.evaluate_script('gon.u2f.challenges')
+
+ json_response = u2f_device(app_id).register_response(challenges[0])
+
+ @page.execute_script("
+ u2f.register = function(appId, registerRequests, signRequests, callback) {
+ callback(#{json_response});
+ };
+ ")
+ end
+
+ def respond_to_u2f_authentication
+ app_id = @page.evaluate_script('gon.u2f.app_id')
+ challenges = @page.evaluate_script('gon.u2f.challenges')
+ json_response = u2f_device(app_id).sign_response(challenges[0])
+
+ @page.execute_script("
+ u2f.sign = function(appId, challenges, signRequests, callback) {
+ callback(#{json_response});
+ };
+ ")
+ end
+
+ private
+
+ def u2f_device(app_id)
+ @u2f_device ||= U2F::FakeU2F.new(app_id)
+ end
+end
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb
index e849a9633b9..a8e454eb09e 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/filter_spec_helper.rb
@@ -40,8 +40,7 @@ module FilterSpecHelper
filters = [
Banzai::Filter::AutolinkFilter,
- described_class,
- Banzai::Filter::ReferenceGathererFilter
+ described_class
]
HTML::Pipeline.new(filters, context)
diff --git a/spec/controllers/import/import_spec_helper.rb b/spec/support/import_spec_helper.rb
index 9d7648e25a7..6710962f082 100644
--- a/spec/controllers/import/import_spec_helper.rb
+++ b/spec/support/import_spec_helper.rb
@@ -28,6 +28,6 @@ module ImportSpecHelper
app_id: 'asd123',
app_secret: 'asd123'
)
- Gitlab.config.omniauth.providers << provider
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
end
end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index cd9fdc6f18e..7a0f078c72b 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -26,11 +26,13 @@ module LoginHelpers
# Internal: Login as the specified user
#
- # user - User instance to login with
- def login_with(user)
+ # user - User instance to login with
+ # remember - Whether or not to check "Remember me" (default: false)
+ def login_with(user, remember: false)
visit new_user_session_path
fill_in "user_login", with: user.email
fill_in "user_password", with: "12345678"
+ check 'user_remember_me' if remember
click_button "Sign in"
Thread.current[:current_user] = user
end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index b87cd6bbca2..a79386b5db9 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -32,6 +32,10 @@ class MarkdownFeature
@project_wiki ||= ProjectWiki.new(project, user)
end
+ def project_wiki_page
+ @project_wiki_page ||= build(:wiki_page, wiki: project_wiki)
+ end
+
def issue
@issue ||= create(:issue, project: project)
end
@@ -63,8 +67,12 @@ class MarkdownFeature
@label ||= create(:label, name: 'awaiting feedback', project: project)
end
+ def simple_milestone
+ @simple_milestone ||= create(:milestone, name: 'gfm-milestone', project: project)
+ end
+
def milestone
- @milestone ||= create(:milestone, project: project)
+ @milestone ||= create(:milestone, name: 'next goal', project: project)
end
# Cross-references -----------------------------------------------------------
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 43cb6ef43f2..e005058ba5b 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -154,7 +154,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-milestone', count: 3)
+ expect(actual).to have_selector('a.gfm.gfm-milestone', count: 6)
end
end
@@ -168,6 +168,16 @@ module MarkdownMatchers
expect(actual).to have_selector('input[checked]', count: 3)
end
end
+
+ # InlineDiffFilter
+ matcher :parse_inline_diffs do
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_selector('span.idiff.addition', count: 2)
+ expect(actual).to have_selector('span.idiff.deletion', count: 2)
+ end
+ end
end
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
diff --git a/spec/support/reference_parser_helpers.rb b/spec/support/reference_parser_helpers.rb
new file mode 100644
index 00000000000..01689194eac
--- /dev/null
+++ b/spec/support/reference_parser_helpers.rb
@@ -0,0 +1,5 @@
+module ReferenceParserHelpers
+ def empty_html_link
+ Nokogiri::HTML.fragment('<a></a>').children[0]
+ end
+end
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index b5ca34bc028..93f96cacc00 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -13,18 +13,35 @@ module StubGitlabCalls
allow_any_instance_of(Network).to receive(:projects) { project_hash_array }
end
- def stub_ci_commit_to_return_yaml_file
- stub_ci_commit_yaml_file(gitlab_ci_yaml)
+ def stub_ci_pipeline_to_return_yaml_file
+ stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
end
- def stub_ci_commit_yaml_file(ci_yaml)
- allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { ci_yaml }
+ def stub_ci_pipeline_yaml_file(ci_yaml)
+ allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml }
end
def stub_ci_builds_disabled
allow_any_instance_of(Project).to receive(:builds_enabled?).and_return(false)
end
+ def stub_container_registry_config(registry_settings)
+ allow(Gitlab.config.registry).to receive_messages(registry_settings)
+ allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
+ end
+
+ def stub_container_registry_tags(*tags)
+ allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_tags).and_return(
+ { "tags" => tags }
+ )
+ allow_any_instance_of(ContainerRegistry::Client).to receive(:repository_manifest).and_return(
+ JSON.load(File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'))
+ )
+ allow_any_instance_of(ContainerRegistry::Client).to receive(:blob).and_return(
+ File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json')
+ )
+ end
+
private
def gitlab_url
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 05fc4c4554f..25da0917134 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
require 'rake'
describe 'gitlab:app namespace rake task' do
+ let(:enable_registry) { true }
+
before :all do
Rake.application.rake_require 'tasks/gitlab/task_helpers'
Rake.application.rake_require 'tasks/gitlab/backup'
@@ -15,13 +17,17 @@ describe 'gitlab:app namespace rake task' do
FileUtils.mkdir_p('public/uploads')
end
+ before do
+ stub_container_registry_config(enabled: enable_registry)
+ end
+
def run_rake_task(task_name)
Rake::Task[task_name].reenable
Rake.application.invoke_task task_name
end
def reenable_backup_sub_tasks
- %w{db repo uploads builds artifacts lfs}.each do |subtask|
+ %w{db repo uploads builds artifacts lfs registry}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -65,6 +71,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
@@ -122,7 +129,7 @@ describe 'gitlab:app namespace rake task' do
it 'should set correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(exit_status).to eq(0)
expect(tar_contents).to match('db/')
@@ -131,16 +138,29 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz)\/$/)
+ expect(tar_contents).to match('registry.tar.gz')
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
end
it 'should delete temp directories' do
temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs}')
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
)
expect(temp_dirs).to be_empty
end
+
+ context 'registry disabled' do
+ let(:enable_registry) { false }
+
+ it 'should not create registry.tar.gz' do
+ tar_contents, exit_status = Gitlab::Popen.popen(
+ %W{tar -tvf #{@backup_tar}}
+ )
+ expect(exit_status).to eq(0)
+ expect(tar_contents).not_to match('registry.tar.gz')
+ end
+ end
end # backup_create task
describe "Skipping items" do
@@ -172,7 +192,7 @@ describe 'gitlab:app namespace rake task' do
it "does not contain skipped item" do
tar_contents, _exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(tar_contents).to match('db/')
@@ -180,6 +200,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
+ expect(tar_contents).to match('registry.tar.gz')
expect(tar_contents).not_to match('repositories/')
end
@@ -195,6 +216,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
new file mode 100644
index 00000000000..36d03a224e4
--- /dev/null
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+require 'rake'
+
+describe 'gitlab:db namespace rake task' do
+ before :all do
+ Rake.application.rake_require 'active_record/railties/databases'
+ Rake.application.rake_require 'tasks/seed_fu'
+ Rake.application.rake_require 'tasks/gitlab/db'
+
+ # empty task as env is already loaded
+ Rake::Task.define_task :environment
+ end
+
+ before do
+ # Stub out db tasks
+ allow(Rake::Task['db:migrate']).to receive(:invoke).and_return(true)
+ allow(Rake::Task['db:schema:load']).to receive(:invoke).and_return(true)
+ allow(Rake::Task['db:seed_fu']).to receive(:invoke).and_return(true)
+ end
+
+ describe 'configure' do
+ it 'should invoke db:migrate when schema has already been loaded' do
+ allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['default'])
+ expect(Rake::Task['db:migrate']).to receive(:invoke)
+ expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
+ expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
+ expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
+ end
+
+ it 'should invoke db:shema:load and db:seed_fu when schema is not loaded' do
+ allow(ActiveRecord::Base.connection).to receive(:tables).and_return([])
+ expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['db:seed_fu']).to receive(:invoke)
+ expect(Rake::Task['db:migrate']).not_to receive(:invoke)
+ expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
+ end
+
+ it 'should not invoke any other rake tasks during an error' do
+ allow(ActiveRecord::Base).to receive(:connection).and_raise(RuntimeError, 'error')
+ expect(Rake::Task['db:migrate']).not_to receive(:invoke)
+ expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
+ expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
+ expect { run_rake_task('gitlab:db:configure') }.to raise_error(RuntimeError, 'error')
+ # unstub connection so that the database cleaner still works
+ allow(ActiveRecord::Base).to receive(:connection).and_call_original
+ end
+
+ it 'should not invoke seed after a failed schema_load' do
+ allow(ActiveRecord::Base.connection).to receive(:tables).and_return([])
+ allow(Rake::Task['db:schema:load']).to receive(:invoke).and_raise(RuntimeError, 'error')
+ expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
+ expect(Rake::Task['db:migrate']).not_to receive(:invoke)
+ expect { run_rake_task('gitlab:db:configure') }.to raise_error(RuntimeError, 'error')
+ end
+ end
+
+ def run_rake_task(task_name)
+ Rake::Task[task_name].reenable
+ Rake.application.invoke_task task_name
+ end
+end
diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb
index 58f45ff8610..69b2b9b6d5b 100644
--- a/spec/teaspoon_env.rb
+++ b/spec/teaspoon_env.rb
@@ -41,11 +41,11 @@ Teaspoon.configure do |config|
suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}"
# Load additional JS files, but requiring them in your spec helper is the preferred way to do this.
- #suite.javascripts = []
+ # suite.javascripts = []
# You can include your own stylesheets if you want to change how Teaspoon looks.
# Note: Spec related CSS can and should be loaded using fixtures.
- #suite.stylesheets = ["teaspoon"]
+ # suite.stylesheets = ["teaspoon"]
# This suites spec helper, which can require additional support files. This file is loaded before any of your test
# files are loaded.
@@ -62,19 +62,19 @@ Teaspoon.configure do |config|
# Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a
# synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name.
- #suite.hook :fixtures, &proc{}
+ # suite.hook :fixtures, &proc{}
# Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated
- # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default,
+ # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default,
# Teaspoon expands all assets to provide more valuable stack traces that reference individual source files.
- #suite.expand_assets = true
+ # suite.expand_assets = true
end
# Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also
# be run in the default suite -- but can be focused into a more specific suite.
- #config.suite :targeted do |suite|
+ # config.suite :targeted do |suite|
# suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}"
- #end
+ # end
# CONSOLE RUNNER SPECIFIC
#
@@ -94,45 +94,45 @@ Teaspoon.configure do |config|
# PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
# Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
# Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit
- #config.driver = :phantomjs
+ # config.driver = :phantomjs
# Specify additional options for the driver.
#
# PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
# Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
# Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit
- #config.driver_options = nil
+ # config.driver_options = nil
# Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be
# considered a failure. This is to avoid issues that can arise where tests stall.
- #config.driver_timeout = 180
+ # config.driver_timeout = 180
# Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used.
- #config.server = nil
+ # config.server = nil
# Specify a port to run on a specific port, otherwise Teaspoon will use a random available port.
- #config.server_port = nil
+ # config.server_port = nil
# Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may
# want to lower this if you know it shouldn't take long to start.
- #config.server_timeout = 20
+ # config.server_timeout = 20
# Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have
# several suites, but in environments like CI this may not be desirable.
- #config.fail_fast = true
+ # config.fail_fast = true
# Specify the formatters to use when outputting the results.
# Note: Output files can be specified by using `"junit>/path/to/output.xml"`.
#
# Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity
- #config.formatters = [:dot]
+ # config.formatters = [:dot]
# Specify if you want color output from the formatters.
- #config.color = true
+ # config.color = true
# Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to
# remove them, but in verbose applications this may not be desirable.
- #config.suppress_log = false
+ # config.suppress_log = false
# COVERAGE REPORTS / THRESHOLD ASSERTIONS
#
@@ -149,7 +149,7 @@ Teaspoon.configure do |config|
# Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage
# on the CLI.
# Set this to "true" or the name of your coverage config.
- #config.use_coverage = nil
+ # config.use_coverage = nil
# You can have multiple coverage configs by passing a name to config.coverage.
# e.g. config.coverage :ci do |coverage|
@@ -158,21 +158,21 @@ Teaspoon.configure do |config|
# Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports.
#
# Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity
- #coverage.reports = ["text-summary", "html"]
+ # coverage.reports = ["text-summary", "html"]
# The path that the coverage should be written to - when there's an artifact to write to disk.
# Note: Relative to `config.root`.
- #coverage.output_path = "coverage"
+ # coverage.output_path = "coverage"
# Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The
# default excludes assets from vendor, gems and support libraries.
- #coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}]
+ # coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}]
# Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any
# aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil.
- #coverage.statements = nil
- #coverage.functions = nil
- #coverage.branches = nil
- #coverage.lines = nil
+ # coverage.statements = nil
+ # coverage.functions = nil
+ # coverage.branches = nil
+ # coverage.lines = nil
end
end
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
new file mode 100644
index 00000000000..e3827cae9a6
--- /dev/null
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe ExpireBuildArtifactsWorker do
+ include RepoHelpers
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ before { build }
+
+ subject! { worker.perform }
+
+ context 'with expired artifacts' do
+ let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) }
+
+ it 'does expire' do
+ expect(build.reload.artifacts_expired?).to be_truthy
+ end
+
+ it 'does remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_falsey
+ end
+ end
+
+ context 'with not yet expired artifacts' do
+ let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) }
+
+ it 'does not expire' do
+ expect(build.reload.artifacts_expired?).to be_falsey
+ end
+
+ it 'does not remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_truthy
+ end
+ end
+
+ context 'without expire date' do
+ let(:build) { create(:ci_build, :artifacts) }
+
+ it 'does not expire' do
+ expect(build.reload.artifacts_expired?).to be_falsey
+ end
+
+ it 'does not remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_truthy
+ end
+ end
+
+ context 'for expired artifacts' do
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.now - 7.days) }
+
+ it 'is still expired' do
+ expect(build.reload.artifacts_expired?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 94ff3457902..b8e73682c91 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -48,6 +48,22 @@ describe PostReceive do
PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
end
+
+ context "gitlab-ci.yml" do
+ subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) }
+
+ context "creates a Ci::Pipeline for every change" do
+ before { stub_ci_pipeline_to_return_yaml_file }
+
+ it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) }
+ end
+
+ context "does not create a Ci::Pipeline" do
+ before { stub_ci_pipeline_yaml_file(nil) }
+
+ it { expect{ subject }.not_to change{ Ci::Pipeline.count } }
+ end
+ end
end
context "webhook" do
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 6739063543b..f1b1574abf4 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -6,14 +6,28 @@ describe RepositoryImportWorker do
subject { described_class.new }
describe '#perform' do
- it 'imports a project' do
- expect_any_instance_of(Projects::ImportService).to receive(:execute).
- and_return({ status: :ok })
+ context 'when the import was successful' do
+ it 'imports a project' do
+ expect_any_instance_of(Projects::ImportService).to receive(:execute).
+ and_return({ status: :ok })
- expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
- expect_any_instance_of(Project).to receive(:import_finish)
+ expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
+ expect_any_instance_of(Project).to receive(:import_finish)
- subject.perform(project.id)
+ subject.perform(project.id)
+ end
+ end
+
+ context 'when the import has failed' do
+ it 'hide the credentials that were used in the import URL' do
+ error = %Q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
+ expect_any_instance_of(Projects::ImportService).to receive(:execute).
+ and_return({ status: :error, message: error })
+
+ subject.perform(project.id)
+
+ expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/")
+ end
end
end
end
diff --git a/spec/workers/stuck_ci_builds_worker_spec.rb b/spec/workers/stuck_ci_builds_worker_spec.rb
index 665ec20f224..801fa31b45d 100644
--- a/spec/workers/stuck_ci_builds_worker_spec.rb
+++ b/spec/workers/stuck_ci_builds_worker_spec.rb
@@ -2,6 +2,7 @@ require "spec_helper"
describe StuckCiBuildsWorker do
let!(:build) { create :ci_build }
+ let(:worker) { described_class.new }
subject do
build.reload
@@ -16,13 +17,13 @@ describe StuckCiBuildsWorker do
it 'gets dropped if it was updated over 2 days ago' do
build.update!(updated_at: 2.days.ago)
- StuckCiBuildsWorker.new.perform
+ worker.perform
is_expected.to eq('failed')
end
it "is still #{status}" do
build.update!(updated_at: 1.minute.ago)
- StuckCiBuildsWorker.new.perform
+ worker.perform
is_expected.to eq(status)
end
end
@@ -36,9 +37,21 @@ describe StuckCiBuildsWorker do
it "is still #{status}" do
build.update!(updated_at: 2.days.ago)
- StuckCiBuildsWorker.new.perform
+ worker.perform
is_expected.to eq(status)
end
end
end
+
+ context "for deleted project" do
+ before do
+ build.update!(status: :running, updated_at: 2.days.ago)
+ build.project.update(pending_delete: true)
+ end
+
+ it "does not drop build" do
+ expect_any_instance_of(Ci::Build).not_to receive(:drop)
+ worker.perform
+ end
+ end
end